feat: migrate imphnen-backend-qr into workspace as imphnen-qr crate
Ports the Go QR campaign overlay service to a self-contained Rust crate nested at /v1/qr/... in the gateway. Features: - Auth: register, login, Google OAuth, JWT refresh (bcrypt compat with Go DB) - Users: profile management + admin CRUD (list/role/delete) - Campaigns: create (auto-generates QR PNG via qrcode crate), list, activate, delete; process-image endpoint overlays active campaign QR onto uploaded images (bottom-right corner, image crate) - QR pool connects to imphnen_qr database via QR_DATABASE_URL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5715e75593
commit
4bba182ea3
@@ -33,6 +33,15 @@ RETRY_ATTEMPTS=3
|
||||
RETRY_DELAY=1
|
||||
GOOGLE_REDIRECT_URL=http://localhost:8000/api/v1/auth/google/callback
|
||||
|
||||
# QR campaign service
|
||||
QR_DATABASE_URL=postgres://imphnen_qr@127.0.0.1:5432/imphnen_qr?sslmode=disable
|
||||
QR_JWT_SECRET=your-qr-jwt-secret-at-least-32-chars
|
||||
QR_JWT_EXPIRY_MINUTES=15
|
||||
QR_JWT_REFRESH_EXPIRY_DAYS=7
|
||||
QR_GOOGLE_CLIENT_ID=your-google-client-id
|
||||
QR_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
QR_GOOGLE_REDIRECT_URL=http://localhost:8080/v1/qr/auth/google/callback
|
||||
|
||||
# Hackathon feature
|
||||
HACKATHON_JWT_SECRET=your-hackathon-jwt-secret-at-least-32-chars
|
||||
HACKATHON_JWT_EXPIRY_HOURS=168
|
||||
|
||||
Generated
+678
@@ -46,6 +46,24 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "aligned"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
|
||||
dependencies = [
|
||||
"as-slice",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aligned-vec"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
|
||||
dependencies = [
|
||||
"equator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
@@ -135,6 +153,17 @@ dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[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 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@@ -153,6 +182,15 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "as-slice"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
@@ -223,6 +261,49 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "av-scenechange"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
|
||||
dependencies = [
|
||||
"aligned",
|
||||
"anyhow",
|
||||
"arg_enum_proc_macro",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"pastey",
|
||||
"rayon",
|
||||
"thiserror 2.0.17",
|
||||
"v_frame",
|
||||
"y4m",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "av1-grain"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"log",
|
||||
"nom",
|
||||
"num-rational",
|
||||
"v_frame",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "avif-serialize"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.7"
|
||||
@@ -353,6 +434,19 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blowfish",
|
||||
"getrandom 0.2.16",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.4.9"
|
||||
@@ -367,6 +461,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
@@ -376,6 +476,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
version = "4.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
|
||||
dependencies = [
|
||||
"core2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
@@ -406,6 +515,16 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.5.7"
|
||||
@@ -429,6 +548,12 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -457,12 +582,24 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[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.11.0"
|
||||
@@ -482,6 +619,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@@ -521,6 +660,22 @@ dependencies = [
|
||||
"stacker",
|
||||
]
|
||||
|
||||
[[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 = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -568,6 +723,15 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -601,6 +765,25 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
@@ -616,6 +799,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -777,6 +966,26 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equator"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
|
||||
dependencies = [
|
||||
"equator-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equator-macro"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -815,12 +1024,56 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.74.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fax"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
|
||||
dependencies = [
|
||||
"fax_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fax_derive"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.5"
|
||||
@@ -1034,6 +1287,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.12"
|
||||
@@ -1053,6 +1316,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1437,6 +1711,46 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"ravif",
|
||||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imgref"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-backend"
|
||||
version = "0.2.0"
|
||||
@@ -1608,12 +1922,14 @@ dependencies = [
|
||||
"imphnen-iam",
|
||||
"imphnen-libs",
|
||||
"imphnen-middleware",
|
||||
"imphnen-qr",
|
||||
"imphnen-utils",
|
||||
"lazy_static",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"utoipa",
|
||||
@@ -1760,6 +2076,30 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-qr"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"image",
|
||||
"imphnen-utils",
|
||||
"jsonwebtoken",
|
||||
"oauth2",
|
||||
"qrcode",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imphnen-utils"
|
||||
version = "0.2.0"
|
||||
@@ -1814,6 +2154,26 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[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 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -1836,6 +2196,15 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -1866,6 +2235,16 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.82"
|
||||
@@ -1900,6 +2279,12 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.19"
|
||||
@@ -1934,6 +2319,16 @@ version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
@@ -1997,6 +2392,15 @@ version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "loop9"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
|
||||
dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
@@ -2018,6 +2422,16 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[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"
|
||||
@@ -2097,6 +2511,16 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
@@ -2131,6 +2555,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 = "8.0.0"
|
||||
@@ -2140,6 +2570,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -2181,6 +2617,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 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -2201,6 +2648,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"
|
||||
@@ -2430,6 +2888,18 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
@@ -2503,6 +2973,19 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
@@ -2631,6 +3114,25 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
||||
dependencies = [
|
||||
"profiling-procmacros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling-procmacros"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.28"
|
||||
@@ -2661,6 +3163,36 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
dependencies = [
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -2812,6 +3344,76 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rav1e"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"arbitrary",
|
||||
"arg_enum_proc_macro",
|
||||
"arrayvec",
|
||||
"av-scenechange",
|
||||
"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",
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.17",
|
||||
"v_frame",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ravif"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
"loop9",
|
||||
"quick-error",
|
||||
"rav1e",
|
||||
"rayon",
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -2912,6 +3514,12 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -3376,6 +3984,15 @@ 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 = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -3852,6 +4469,20 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.44"
|
||||
@@ -4275,6 +4906,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_frame"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -4420,6 +5062,12 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -4776,6 +5424,12 @@ dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
@@ -4950,3 +5604,27 @@ dependencies = [
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[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.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ members = [
|
||||
"imphnen-gacha", # Game mechanics, depends on core services
|
||||
"imphnen-dimentorin",# Learning platform, depends on core services
|
||||
"imphnen-hackathon", # Hackathon feature, standalone with Supabase auth
|
||||
"imphnen-qr", # QR campaign overlay service
|
||||
"imphnen-gateway", # API gateway, depends on all services
|
||||
"imphnen-backend", # Main application, depends on all services
|
||||
]
|
||||
@@ -89,6 +90,10 @@ imphnen-dimentorin = { path = "./imphnen-dimentorin" }
|
||||
imphnen-middleware = { path = "./imphnen-middleware" }
|
||||
imphnen-macros = { path = "./imphnen-macros" }
|
||||
imphnen-hackathon = { path = "./imphnen-hackathon" }
|
||||
imphnen-qr = { path = "./imphnen-qr" }
|
||||
bcrypt = "0.15"
|
||||
image = { version = "0.25", features = ["png", "jpeg"] }
|
||||
qrcode = { version = "0.14", default-features = false, features = ["image"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
||||
@@ -5,6 +5,8 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-hackathon.workspace = true
|
||||
imphnen-qr.workspace = true
|
||||
sqlx.workspace = true
|
||||
imphnen-iam.workspace = true
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
|
||||
@@ -17,6 +17,7 @@ use imphnen_dimentorin::{
|
||||
};
|
||||
use imphnen_gacha::gacha_router;
|
||||
use imphnen_hackathon::{hackathon_router, HackathonConfig};
|
||||
use imphnen_qr::{qr_router, QrConfig};
|
||||
use imphnen_iam::{
|
||||
auth_public_routes,
|
||||
permissions_protected_routes,
|
||||
@@ -45,6 +46,14 @@ pub async fn gateway_service(
|
||||
let db = state.postgres_connection.conn.clone();
|
||||
let state_arc = Arc::new(state.clone());
|
||||
let hackathon_config = Arc::new(HackathonConfig::from_env());
|
||||
let qr_config = Arc::new(QrConfig::from_env());
|
||||
let qr_pool = Arc::new(
|
||||
sqlx::PgPool::connect(
|
||||
&std::env::var("QR_DATABASE_URL").expect("QR_DATABASE_URL must be set"),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to connect to QR database"),
|
||||
);
|
||||
|
||||
let public_routes = Router::new()
|
||||
.merge(auth_public_routes(db.clone(), Arc::clone(&state_arc)).layer(from_fn(rate_limiting_middleware)))
|
||||
@@ -68,6 +77,7 @@ pub async fn gateway_service(
|
||||
.route("/", get(Redirect::to("/docs")))
|
||||
.nest("/v1", public_routes.merge(protected_routes))
|
||||
.nest("/v1/hackathon", hackathon_router(db.clone(), hackathon_config))
|
||||
.nest("/v1/qr", qr_router(qr_pool, qr_config))
|
||||
.merge(SwaggerUi::new("/docs").url("/openapi.json", docs_router()))
|
||||
.layer(cors_middleware())
|
||||
.layer(from_fn(security_headers_middleware))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "imphnen-qr"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-utils.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
bcrypt.workspace = true
|
||||
chrono.workspace = true
|
||||
uuid.workspace = true
|
||||
sqlx.workspace = true
|
||||
reqwest.workspace = true
|
||||
oauth2.workspace = true
|
||||
tracing.workspace = true
|
||||
utoipa.workspace = true
|
||||
image.workspace = true
|
||||
qrcode.workspace = true
|
||||
@@ -0,0 +1,154 @@
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use sqlx::PgPool;
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use crate::common::qr_jwt::QrJwtService;
|
||||
use crate::config::QrConfig;
|
||||
use super::super::domain::service::{QrAuthService, AuthTokens, QrUserData};
|
||||
|
||||
pub struct QrAuthServiceImpl {
|
||||
pool: Arc<PgPool>,
|
||||
jwt: Arc<QrJwtService>,
|
||||
config: Arc<QrConfig>,
|
||||
}
|
||||
|
||||
impl QrAuthServiceImpl {
|
||||
pub fn new(pool: Arc<PgPool>, jwt: Arc<QrJwtService>, config: Arc<QrConfig>) -> Self {
|
||||
Self { pool, jwt, config }
|
||||
}
|
||||
|
||||
async fn find_user_by_id(&self, id: Uuid) -> Result<QrUserData, AppError> {
|
||||
sqlx::query_as::<_, QrUserData>(
|
||||
"SELECT id, email, name, role, provider, created_at, updated_at FROM users WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("User not found".to_string()))
|
||||
}
|
||||
|
||||
async fn find_user_by_email(&self, email: &str) -> Result<Option<serde_json::Value>, AppError> {
|
||||
sqlx::query_scalar::<_, serde_json::Value>(
|
||||
"SELECT row_to_json(u) FROM (SELECT id, email, name, role, provider, password FROM users WHERE email = $1) u"
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
fn make_tokens(&self, user_id: Uuid, role: &str) -> Result<AuthTokens, AppError> {
|
||||
Ok(AuthTokens {
|
||||
access_token: self.jwt.generate_token(user_id, role)?,
|
||||
refresh_token: self.jwt.generate_refresh_token(user_id, role)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QrAuthService for QrAuthServiceImpl {
|
||||
async fn register(&self, email: String, password: String, name: String) -> Result<(AuthTokens, QrUserData), AppError> {
|
||||
let existing = self.find_user_by_email(&email).await?;
|
||||
if existing.is_some() {
|
||||
return Err(AppError::ConflictError("Email already registered".to_string()));
|
||||
}
|
||||
let hashed = bcrypt::hash(&password, 10)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let user = sqlx::query_as::<_, QrUserData>(
|
||||
"INSERT INTO users (email, password, name, role, provider) VALUES ($1, $2, $3, 'user', 'local') RETURNING id, email, name, role, provider, created_at, updated_at"
|
||||
)
|
||||
.bind(&email)
|
||||
.bind(&hashed)
|
||||
.bind(&name)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let tokens = self.make_tokens(user.id, &user.role)?;
|
||||
Ok((tokens, user))
|
||||
}
|
||||
|
||||
async fn login(&self, email: String, password: String) -> Result<(AuthTokens, QrUserData), AppError> {
|
||||
let row = self.find_user_by_email(&email).await?
|
||||
.ok_or_else(|| AppError::AuthenticationError("Invalid credentials".to_string()))?;
|
||||
let provider = row["provider"].as_str().unwrap_or("local");
|
||||
if provider != "local" {
|
||||
return Err(AppError::AuthenticationError("Account uses social login".to_string()));
|
||||
}
|
||||
let stored_hash = row["password"].as_str()
|
||||
.ok_or_else(|| AppError::AuthenticationError("Invalid credentials".to_string()))?;
|
||||
let valid = bcrypt::verify(&password, stored_hash)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
if !valid {
|
||||
return Err(AppError::AuthenticationError("Invalid credentials".to_string()));
|
||||
}
|
||||
let user_id: Uuid = row["id"].as_str()
|
||||
.and_then(|s| Uuid::parse_str(s).ok())
|
||||
.ok_or_else(|| AppError::InternalServerError("Invalid user ID".to_string()))?;
|
||||
let user = self.find_user_by_id(user_id).await?;
|
||||
let tokens = self.make_tokens(user.id, &user.role)?;
|
||||
Ok((tokens, user))
|
||||
}
|
||||
|
||||
async fn google_callback(&self, code: String) -> Result<(AuthTokens, QrUserData), AppError> {
|
||||
let http = reqwest::Client::new();
|
||||
let token_res: serde_json::Value = http
|
||||
.post("https://oauth2.googleapis.com/token")
|
||||
.form(&[
|
||||
("code", code.as_str()),
|
||||
("client_id", self.config.google_client_id.as_str()),
|
||||
("client_secret", self.config.google_client_secret.as_str()),
|
||||
("redirect_uri", self.config.google_redirect_url.as_str()),
|
||||
("grant_type", "authorization_code"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
if token_res.get("error").is_some() {
|
||||
return Err(AppError::BadRequestError("Google OAuth error".to_string()));
|
||||
}
|
||||
let access_token = token_res["access_token"].as_str()
|
||||
.ok_or_else(|| AppError::InternalServerError("Missing access token from Google".to_string()))?;
|
||||
let google_user: serde_json::Value = http
|
||||
.get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let email = google_user["email"].as_str()
|
||||
.ok_or_else(|| AppError::InternalServerError("Missing email from Google".to_string()))?;
|
||||
let name = google_user["name"].as_str().unwrap_or(email);
|
||||
let provider_id = google_user["id"].as_str().unwrap_or("");
|
||||
let user = sqlx::query_as::<_, QrUserData>(
|
||||
"INSERT INTO users (email, name, role, provider, provider_id) VALUES ($1, $2, 'user', 'google', $3)
|
||||
ON CONFLICT (email) DO UPDATE SET provider_id = EXCLUDED.provider_id, updated_at = NOW()
|
||||
RETURNING id, email, name, role, provider, created_at, updated_at"
|
||||
)
|
||||
.bind(email)
|
||||
.bind(name)
|
||||
.bind(provider_id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let tokens = self.make_tokens(user.id, &user.role)?;
|
||||
Ok((tokens, user))
|
||||
}
|
||||
|
||||
async fn refresh_token(&self, refresh_token: String) -> Result<AuthTokens, AppError> {
|
||||
let claims = self.jwt.verify_token(&refresh_token)?;
|
||||
let user_id = Uuid::parse_str(&claims.sub)
|
||||
.map_err(|_| AppError::AuthenticationError("Invalid token subject".to_string()))?;
|
||||
let user = self.find_user_by_id(user_id).await?;
|
||||
Ok(AuthTokens {
|
||||
access_token: self.jwt.generate_token(user.id, &user.role)?,
|
||||
refresh_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod auth_service;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod service;
|
||||
@@ -0,0 +1,30 @@
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::errors::AppError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthTokens {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, sqlx::FromRow)]
|
||||
pub struct QrUserData {
|
||||
pub id: Uuid,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub provider: String,
|
||||
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait QrAuthService: Send + Sync {
|
||||
async fn register(&self, email: String, password: String, name: String) -> Result<(AuthTokens, QrUserData), AppError>;
|
||||
async fn login(&self, email: String, password: String) -> Result<(AuthTokens, QrUserData), AppError>;
|
||||
async fn google_callback(&self, code: String) -> Result<(AuthTokens, QrUserData), AppError>;
|
||||
async fn refresh_token(&self, refresh_token: String) -> Result<AuthTokens, AppError>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use crate::auth::domain::service::QrUserData;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RegisterRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RefreshRequest {
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AuthResponse {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub user: QrUserData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TokensResponse {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use axum::{Extension, Json, response::IntoResponse};
|
||||
use axum::extract::Query;
|
||||
use std::sync::Arc;
|
||||
use serde::Deserialize;
|
||||
use imphnen_utils::response_format::ApiSuccess;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use crate::auth::domain::service::QrAuthService;
|
||||
use crate::config::QrConfig;
|
||||
use super::dto::{RegisterRequest, LoginRequest, RefreshRequest, AuthResponse, TokensResponse};
|
||||
|
||||
pub async fn register_handler(
|
||||
Extension(service): Extension<Arc<dyn QrAuthService>>,
|
||||
Json(body): Json<RegisterRequest>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let (tokens, user) = service.register(body.email, body.password, body.name).await?;
|
||||
Ok(ApiSuccess(AuthResponse {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
user,
|
||||
}).into_response())
|
||||
}
|
||||
|
||||
pub async fn login_handler(
|
||||
Extension(service): Extension<Arc<dyn QrAuthService>>,
|
||||
Json(body): Json<LoginRequest>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let (tokens, user) = service.login(body.email, body.password).await?;
|
||||
Ok(ApiSuccess(AuthResponse {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
user,
|
||||
}).into_response())
|
||||
}
|
||||
|
||||
pub async fn google_redirect_handler(
|
||||
Extension(config): Extension<Arc<QrConfig>>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let url = format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&redirect_uri={}&response_type=code&scope=email+profile",
|
||||
config.google_client_id,
|
||||
config.google_redirect_url,
|
||||
);
|
||||
Ok(axum::response::Redirect::temporary(&url).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GoogleCallbackQuery {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
pub async fn google_callback_handler(
|
||||
Extension(service): Extension<Arc<dyn QrAuthService>>,
|
||||
Query(params): Query<GoogleCallbackQuery>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let (tokens, user) = service.google_callback(params.code).await?;
|
||||
Ok(ApiSuccess(AuthResponse {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
user,
|
||||
}).into_response())
|
||||
}
|
||||
|
||||
pub async fn refresh_handler(
|
||||
Extension(service): Extension<Arc<dyn QrAuthService>>,
|
||||
Json(body): Json<RefreshRequest>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let tokens = service.refresh_token(body.refresh_token).await?;
|
||||
Ok(ApiSuccess(TokensResponse {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
}).into_response())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
@@ -0,0 +1,29 @@
|
||||
use axum::{routing::{get, post}, Extension, Router};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use crate::auth::application::auth_service::QrAuthServiceImpl;
|
||||
use crate::auth::domain::service::QrAuthService;
|
||||
use crate::common::qr_jwt::QrJwtService;
|
||||
use crate::config::QrConfig;
|
||||
use super::handlers::{
|
||||
register_handler,
|
||||
login_handler,
|
||||
google_redirect_handler,
|
||||
google_callback_handler,
|
||||
refresh_handler,
|
||||
};
|
||||
|
||||
pub fn qr_auth_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>, config: Arc<QrConfig>) -> Router {
|
||||
let service: Arc<dyn QrAuthService> = Arc::new(
|
||||
QrAuthServiceImpl::new(pool, jwt, config.clone())
|
||||
);
|
||||
|
||||
Router::new()
|
||||
.route("/auth/register", post(register_handler))
|
||||
.route("/auth/login", post(login_handler))
|
||||
.route("/auth/google", get(google_redirect_handler))
|
||||
.route("/auth/google/callback", get(google_callback_handler))
|
||||
.route("/auth/refresh", post(refresh_handler))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension(config))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod domain;
|
||||
pub mod application;
|
||||
pub mod infrastructure;
|
||||
@@ -0,0 +1,88 @@
|
||||
use async_trait::async_trait;
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, imageops};
|
||||
use imphnen_utils::errors::AppError;
|
||||
use qrcode::QrCode;
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::campaigns::domain::{
|
||||
entity::{CampaignEntity, CreateCampaignInput},
|
||||
repository::CampaignRepository,
|
||||
service::QrCampaignService,
|
||||
};
|
||||
|
||||
pub struct QrCampaignServiceImpl {
|
||||
repo: Arc<dyn CampaignRepository>,
|
||||
}
|
||||
|
||||
impl QrCampaignServiceImpl {
|
||||
pub fn new(repo: Arc<dyn CampaignRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QrCampaignService for QrCampaignServiceImpl {
|
||||
async fn create(&self, name: String, url: String, created_by: Uuid) -> Result<CampaignEntity, AppError> {
|
||||
let qr = QrCode::new(url.as_bytes())
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
let qr_img = qr.render::<image::Luma<u8>>().min_dimensions(256, 256).build();
|
||||
let mut qr_bytes = Vec::new();
|
||||
DynamicImage::ImageLuma8(qr_img)
|
||||
.write_to(&mut Cursor::new(&mut qr_bytes), ImageFormat::Png)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let input = CreateCampaignInput {
|
||||
name,
|
||||
url,
|
||||
created_by,
|
||||
qr_code_data: qr_bytes,
|
||||
};
|
||||
self.repo.create(input).await
|
||||
}
|
||||
|
||||
async fn list_all(&self) -> Result<Vec<CampaignEntity>, AppError> {
|
||||
self.repo.find_all().await
|
||||
}
|
||||
|
||||
async fn get_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError> {
|
||||
self.repo.find_active_qr_data().await
|
||||
}
|
||||
|
||||
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError> {
|
||||
self.repo.set_active(id).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.delete(id).await
|
||||
}
|
||||
|
||||
async fn process_image(&self, image_bytes: Vec<u8>) -> Result<Vec<u8>, AppError> {
|
||||
let qr_data = self.repo.find_active_qr_data().await?
|
||||
.ok_or_else(|| AppError::NotFoundError("No active campaign".to_string()))?;
|
||||
|
||||
let img = image::load_from_memory(&image_bytes)
|
||||
.map_err(|_| AppError::BadRequestError("Invalid image format".to_string()))?;
|
||||
|
||||
let qr_img = image::load_from_memory(&qr_data)
|
||||
.map_err(|_| AppError::InternalServerError("Failed to load QR data".to_string()))?;
|
||||
|
||||
let (w, h) = img.dimensions();
|
||||
let qr_size = (std::cmp::min(w, h) / 5).max(100);
|
||||
|
||||
let qr_resized = qr_img.resize_exact(qr_size, qr_size, imageops::FilterType::Nearest);
|
||||
|
||||
let mut output = img.to_rgba8();
|
||||
let x = (w - qr_size - 10) as i64;
|
||||
let y = (h - qr_size - 10) as i64;
|
||||
imageops::overlay(&mut output, &qr_resized.to_rgba8(), x, y);
|
||||
|
||||
let mut out_bytes = Vec::new();
|
||||
DynamicImage::ImageRgba8(output)
|
||||
.write_to(&mut Cursor::new(&mut out_bytes), ImageFormat::Png)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(out_bytes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod campaign_service;
|
||||
@@ -0,0 +1,24 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, FromRow, Clone)]
|
||||
pub struct CampaignEntity {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub is_active: bool,
|
||||
pub created_by: Uuid,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub struct CreateCampaignInput {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub created_by: Uuid,
|
||||
pub qr_code_data: Vec<u8>,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod entity;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
@@ -0,0 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::entity::{CampaignEntity, CreateCampaignInput};
|
||||
|
||||
#[async_trait]
|
||||
pub trait CampaignRepository: Send + Sync {
|
||||
async fn create(&self, input: CreateCampaignInput) -> Result<CampaignEntity, AppError>;
|
||||
async fn find_all(&self) -> Result<Vec<CampaignEntity>, AppError>;
|
||||
async fn find_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError>;
|
||||
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::entity::CampaignEntity;
|
||||
|
||||
#[async_trait]
|
||||
pub trait QrCampaignService: Send + Sync {
|
||||
async fn create(&self, name: String, url: String, created_by: Uuid) -> Result<CampaignEntity, AppError>;
|
||||
async fn list_all(&self) -> Result<Vec<CampaignEntity>, AppError>;
|
||||
async fn get_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError>;
|
||||
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
async fn process_image(&self, image_bytes: Vec<u8>) -> Result<Vec<u8>, AppError>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateCampaignRequest {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct CampaignResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub is_active: bool,
|
||||
pub created_by: Uuid,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use axum::{
|
||||
extract::{Multipart, Path},
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Json,
|
||||
};
|
||||
use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
campaigns::{
|
||||
domain::service::QrCampaignService,
|
||||
infrastructure::http::dto::CreateCampaignRequest,
|
||||
},
|
||||
middleware::qr_auth::QrAuthUser,
|
||||
};
|
||||
|
||||
pub async fn create_campaign_handler(
|
||||
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
Json(body): Json<CreateCampaignRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
let campaign = service.create(body.name, body.url, auth_user.user_id).await?;
|
||||
Ok(imphnen_utils::response_format::ApiCreated(campaign).into_response())
|
||||
}
|
||||
|
||||
pub async fn list_campaigns_handler(
|
||||
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
let campaigns = service.list_all().await?;
|
||||
Ok(ApiSuccess(campaigns).into_response())
|
||||
}
|
||||
|
||||
pub async fn activate_campaign_handler(
|
||||
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
let campaign = service.set_active(id).await?;
|
||||
Ok(ApiSuccess(campaign).into_response())
|
||||
}
|
||||
|
||||
pub async fn delete_campaign_handler(
|
||||
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
service.delete(id).await?;
|
||||
Ok(imphnen_utils::response_format::ApiMessage::ok("Campaign deleted successfully").into_response())
|
||||
}
|
||||
|
||||
pub async fn process_image_handler(
|
||||
Extension(service): Extension<Arc<dyn QrCampaignService>>,
|
||||
Extension(_auth_user): Extension<QrAuthUser>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Response, AppError> {
|
||||
let mut image_bytes = Vec::new();
|
||||
while let Some(field) = multipart.next_field().await.map_err(|e| AppError::BadRequestError(e.to_string()))? {
|
||||
if field.name() == Some("file") {
|
||||
image_bytes = field.bytes().await.map_err(|e| AppError::BadRequestError(e.to_string()))?.to_vec();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if image_bytes.is_empty() {
|
||||
return Err(AppError::BadRequestError("No file provided".to_string()));
|
||||
}
|
||||
let png_bytes = service.process_image(image_bytes).await?;
|
||||
Ok((
|
||||
[(axum::http::header::CONTENT_TYPE, "image/png")],
|
||||
png_bytes,
|
||||
).into_response())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
@@ -0,0 +1,38 @@
|
||||
use axum::{
|
||||
middleware::from_fn,
|
||||
routing::{delete, post, put},
|
||||
Extension, Router,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
campaigns::{
|
||||
application::campaign_service::QrCampaignServiceImpl,
|
||||
domain::{repository::CampaignRepository, service::QrCampaignService},
|
||||
infrastructure::{
|
||||
http::handlers::{
|
||||
activate_campaign_handler, create_campaign_handler, delete_campaign_handler,
|
||||
list_campaigns_handler, process_image_handler,
|
||||
},
|
||||
persistence::postgres_campaign_repository::PostgresCampaignRepository,
|
||||
},
|
||||
},
|
||||
common::qr_jwt::QrJwtService,
|
||||
middleware::qr_auth::qr_auth_middleware,
|
||||
};
|
||||
|
||||
pub fn qr_campaigns_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>) -> Router {
|
||||
let repo: Arc<dyn CampaignRepository> = Arc::new(PostgresCampaignRepository::new(pool.clone()));
|
||||
let service: Arc<dyn QrCampaignService> = Arc::new(QrCampaignServiceImpl::new(repo));
|
||||
|
||||
Router::new()
|
||||
.route("/campaigns", post(create_campaign_handler).get(list_campaigns_handler))
|
||||
.route("/campaigns/:id/activate", put(activate_campaign_handler))
|
||||
.route("/campaigns/:id", delete(delete_campaign_handler))
|
||||
.route("/campaigns/process-image", post(process_image_handler))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension(jwt.clone()))
|
||||
.layer(Extension(pool))
|
||||
.layer(from_fn(qr_auth_middleware))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod postgres_campaign_repository;
|
||||
@@ -0,0 +1,107 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::campaigns::domain::{
|
||||
entity::{CampaignEntity, CreateCampaignInput},
|
||||
repository::CampaignRepository,
|
||||
};
|
||||
|
||||
pub struct PostgresCampaignRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl PostgresCampaignRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CampaignRepository for PostgresCampaignRepository {
|
||||
async fn create(&self, input: CreateCampaignInput) -> Result<CampaignEntity, AppError> {
|
||||
let mut tx = self.pool.begin().await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
sqlx::query("UPDATE qr_campaigns SET is_active = false, updated_at = NOW()")
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
let campaign = sqlx::query_as::<_, CampaignEntity>(
|
||||
"INSERT INTO qr_campaigns (id, name, url, qr_code_data, is_active, created_by, expires_at) \
|
||||
VALUES ($1, $2, $3, $4, true, $5, NOW() + INTERVAL '30 days') \
|
||||
RETURNING id, name, url, is_active, created_by, expires_at, created_at, updated_at",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&input.name)
|
||||
.bind(&input.url)
|
||||
.bind(&input.qr_code_data)
|
||||
.bind(input.created_by)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
tx.commit().await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(campaign)
|
||||
}
|
||||
|
||||
async fn find_all(&self) -> Result<Vec<CampaignEntity>, AppError> {
|
||||
sqlx::query_as::<_, CampaignEntity>(
|
||||
"SELECT id, name, url, is_active, created_by, expires_at, created_at, updated_at \
|
||||
FROM qr_campaigns ORDER BY created_at DESC",
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn find_active_qr_data(&self) -> Result<Option<Vec<u8>>, AppError> {
|
||||
let row = sqlx::query_as::<_, (Vec<u8>,)>(
|
||||
"SELECT qr_code_data FROM qr_campaigns WHERE is_active = true LIMIT 1",
|
||||
)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(row.map(|r| r.0))
|
||||
}
|
||||
|
||||
async fn set_active(&self, id: Uuid) -> Result<CampaignEntity, AppError> {
|
||||
let mut tx = self.pool.begin().await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
sqlx::query("UPDATE qr_campaigns SET is_active = false, updated_at = NOW()")
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
let campaign = sqlx::query_as::<_, CampaignEntity>(
|
||||
"UPDATE qr_campaigns SET is_active = true, updated_at = NOW() WHERE id = $1 \
|
||||
RETURNING id, name, url, is_active, created_by, expires_at, created_at, updated_at",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
tx.commit().await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
Ok(campaign)
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("DELETE FROM qr_campaigns WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod domain;
|
||||
pub mod application;
|
||||
pub mod infrastructure;
|
||||
pub use infrastructure::http::routes::qr_campaigns_routes;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod qr_jwt;
|
||||
@@ -0,0 +1,59 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::errors::AppError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct QrClaims {
|
||||
pub sub: String,
|
||||
pub role: String,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QrJwtService {
|
||||
encoding_key: EncodingKey,
|
||||
decoding_key: DecodingKey,
|
||||
expiry_minutes: i64,
|
||||
refresh_expiry_days: i64,
|
||||
}
|
||||
|
||||
impl QrJwtService {
|
||||
pub fn new(secret: &str, expiry_minutes: i64, refresh_expiry_days: i64) -> Self {
|
||||
Self {
|
||||
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
|
||||
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
|
||||
expiry_minutes,
|
||||
refresh_expiry_days,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_token(&self, user_id: Uuid, role: &str) -> Result<String, AppError> {
|
||||
let exp = (Utc::now() + Duration::minutes(self.expiry_minutes)).timestamp() as usize;
|
||||
let claims = QrClaims {
|
||||
sub: user_id.to_string(),
|
||||
role: role.to_string(),
|
||||
exp,
|
||||
};
|
||||
encode(&Header::default(), &claims, &self.encoding_key)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn generate_refresh_token(&self, user_id: Uuid, role: &str) -> Result<String, AppError> {
|
||||
let exp = (Utc::now() + Duration::days(self.refresh_expiry_days)).timestamp() as usize;
|
||||
let claims = QrClaims {
|
||||
sub: user_id.to_string(),
|
||||
role: role.to_string(),
|
||||
exp,
|
||||
};
|
||||
encode(&Header::default(), &claims, &self.encoding_key)
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn verify_token(&self, token: &str) -> Result<QrClaims, AppError> {
|
||||
decode::<QrClaims>(token, &self.decoding_key, &Validation::default())
|
||||
.map(|d| d.claims)
|
||||
.map_err(|_| AppError::AuthenticationError("Invalid or expired token".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QrConfig {
|
||||
pub jwt_secret: String,
|
||||
pub jwt_expiry_minutes: i64,
|
||||
pub refresh_expiry_days: i64,
|
||||
pub google_client_id: String,
|
||||
pub google_client_secret: String,
|
||||
pub google_redirect_url: String,
|
||||
}
|
||||
|
||||
impl QrConfig {
|
||||
pub fn from_env() -> Self {
|
||||
Self {
|
||||
jwt_secret: env::var("QR_JWT_SECRET").expect("QR_JWT_SECRET must be set"),
|
||||
jwt_expiry_minutes: env::var("QR_JWT_EXPIRY_MINUTES")
|
||||
.unwrap_or_else(|_| "15".to_string())
|
||||
.parse()
|
||||
.unwrap_or(15),
|
||||
refresh_expiry_days: env::var("QR_JWT_REFRESH_EXPIRY_DAYS")
|
||||
.unwrap_or_else(|_| "7".to_string())
|
||||
.parse()
|
||||
.unwrap_or(7),
|
||||
google_client_id: env::var("QR_GOOGLE_CLIENT_ID").unwrap_or_default(),
|
||||
google_client_secret: env::var("QR_GOOGLE_CLIENT_SECRET").unwrap_or_default(),
|
||||
google_redirect_url: env::var("QR_GOOGLE_REDIRECT_URL").unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
pub mod config;
|
||||
pub mod common;
|
||||
pub mod middleware;
|
||||
pub mod auth;
|
||||
pub mod users;
|
||||
pub mod campaigns;
|
||||
|
||||
pub use config::QrConfig;
|
||||
|
||||
use axum::Router;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use common::qr_jwt::QrJwtService;
|
||||
|
||||
pub fn qr_router(pool: Arc<PgPool>, config: Arc<QrConfig>) -> Router {
|
||||
let jwt = Arc::new(QrJwtService::new(
|
||||
&config.jwt_secret,
|
||||
config.jwt_expiry_minutes,
|
||||
config.refresh_expiry_days,
|
||||
));
|
||||
|
||||
Router::new()
|
||||
.merge(auth::infrastructure::http::routes::qr_auth_routes(pool.clone(), jwt.clone(), config.clone()))
|
||||
.merge(users::infrastructure::http::routes::qr_users_routes(pool.clone(), jwt.clone()))
|
||||
.merge(campaigns::infrastructure::http::routes::qr_campaigns_routes(pool.clone(), jwt.clone()))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod qr_auth;
|
||||
@@ -0,0 +1,39 @@
|
||||
use axum::{body::Body, extract::Request, middleware::Next, response::{IntoResponse, Response}};
|
||||
use axum::http::StatusCode;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::common::qr_jwt::QrJwtService;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QrAuthUser {
|
||||
pub user_id: Uuid,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
pub async fn qr_auth_middleware(
|
||||
axum::Extension(jwt_service): axum::Extension<Arc<QrJwtService>>,
|
||||
mut request: Request<Body>,
|
||||
next: Next,
|
||||
) -> Result<Response, Response> {
|
||||
let auth_header = request
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.ok_or_else(|| (StatusCode::UNAUTHORIZED, "Missing Authorization header").into_response())?;
|
||||
|
||||
let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
|
||||
(StatusCode::UNAUTHORIZED, "Invalid Authorization header format").into_response()
|
||||
})?;
|
||||
|
||||
let claims = jwt_service.verify_token(token).map_err(|_| {
|
||||
(StatusCode::UNAUTHORIZED, "Invalid or expired token").into_response()
|
||||
})?;
|
||||
|
||||
let user_id = Uuid::parse_str(&claims.sub).map_err(|_| {
|
||||
(StatusCode::UNAUTHORIZED, "Invalid user ID in token").into_response()
|
||||
})?;
|
||||
|
||||
request.extensions_mut().insert(QrAuthUser { user_id, role: claims.role });
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod user_service;
|
||||
@@ -0,0 +1,51 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::users::domain::{
|
||||
entity::{UpdateUserInput, UserEntity},
|
||||
repository::UserRepository,
|
||||
service::QrUserService,
|
||||
};
|
||||
|
||||
pub struct QrUserServiceImpl {
|
||||
repo: Arc<dyn UserRepository>,
|
||||
}
|
||||
|
||||
impl QrUserServiceImpl {
|
||||
pub fn new(repo: Arc<dyn UserRepository>) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QrUserService for QrUserServiceImpl {
|
||||
async fn get_profile(&self, user_id: Uuid) -> Result<UserEntity, AppError> {
|
||||
self.repo
|
||||
.find_by_id(user_id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFoundError("User not found".to_string()))
|
||||
}
|
||||
|
||||
async fn update_profile(&self, user_id: Uuid, input: UpdateUserInput) -> Result<UserEntity, AppError> {
|
||||
if let Some(ref email) = input.email {
|
||||
if email.trim().is_empty() {
|
||||
return Err(AppError::ValidationError("Email cannot be empty".to_string()));
|
||||
}
|
||||
}
|
||||
self.repo.update(user_id, input).await
|
||||
}
|
||||
|
||||
async fn list_all(&self) -> Result<Vec<UserEntity>, AppError> {
|
||||
self.repo.find_all().await
|
||||
}
|
||||
|
||||
async fn update_role(&self, id: Uuid, role: String) -> Result<UserEntity, AppError> {
|
||||
self.repo.update_role(id, role).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
self.repo.delete(id).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, FromRow, Clone)]
|
||||
pub struct UserEntity {
|
||||
pub id: Uuid,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub provider: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub struct UpdateUserInput {
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod entity;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
@@ -0,0 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::entity::{UpdateUserInput, UserEntity};
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<UserEntity>, AppError>;
|
||||
async fn find_all(&self) -> Result<Vec<UserEntity>, AppError>;
|
||||
async fn update(&self, id: Uuid, input: UpdateUserInput) -> Result<UserEntity, AppError>;
|
||||
async fn update_role(&self, id: Uuid, role: String) -> Result<UserEntity, AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::entity::{UpdateUserInput, UserEntity};
|
||||
|
||||
#[async_trait]
|
||||
pub trait QrUserService: Send + Sync {
|
||||
async fn get_profile(&self, user_id: Uuid) -> Result<UserEntity, AppError>;
|
||||
async fn update_profile(&self, user_id: Uuid, input: UpdateUserInput) -> Result<UserEntity, AppError>;
|
||||
async fn list_all(&self) -> Result<Vec<UserEntity>, AppError>;
|
||||
async fn update_role(&self, id: Uuid, role: String) -> Result<UserEntity, AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateProfileRequest {
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateRoleRequest {
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct UserResponse {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub provider: String,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use axum::{
|
||||
extract::Path,
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Json,
|
||||
};
|
||||
use imphnen_utils::{errors::AppError, response_format::ApiSuccess};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
middleware::qr_auth::QrAuthUser,
|
||||
users::{
|
||||
domain::{entity::UpdateUserInput, service::QrUserService},
|
||||
infrastructure::http::dto::{UpdateProfileRequest, UpdateRoleRequest},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn get_me_handler(
|
||||
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
let user = service.get_profile(auth_user.user_id).await?;
|
||||
Ok(ApiSuccess(user).into_response())
|
||||
}
|
||||
|
||||
pub async fn update_me_handler(
|
||||
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
Json(body): Json<UpdateProfileRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let input = UpdateUserInput {
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
};
|
||||
let user = service.update_profile(auth_user.user_id, input).await?;
|
||||
Ok(ApiSuccess(user).into_response())
|
||||
}
|
||||
|
||||
pub async fn list_users_handler(
|
||||
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
let users = service.list_all().await?;
|
||||
Ok(ApiSuccess(users).into_response())
|
||||
}
|
||||
|
||||
pub async fn update_role_handler(
|
||||
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateRoleRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
let user = service.update_role(id, body.role).await?;
|
||||
Ok(ApiSuccess(user).into_response())
|
||||
}
|
||||
|
||||
pub async fn delete_user_handler(
|
||||
Extension(service): Extension<Arc<dyn QrUserService>>,
|
||||
Extension(auth_user): Extension<QrAuthUser>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Response, AppError> {
|
||||
if auth_user.role != "admin" {
|
||||
return Err(AppError::ForbiddenError("Admin access required".to_string()));
|
||||
}
|
||||
service.delete(id).await?;
|
||||
Ok(imphnen_utils::response_format::ApiMessage::ok("User deleted successfully").into_response())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod dto;
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
@@ -0,0 +1,38 @@
|
||||
use axum::{
|
||||
middleware::from_fn,
|
||||
routing::{delete, get, put},
|
||||
Extension, Router,
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
common::qr_jwt::QrJwtService,
|
||||
middleware::qr_auth::qr_auth_middleware,
|
||||
users::{
|
||||
application::user_service::QrUserServiceImpl,
|
||||
domain::{repository::UserRepository, service::QrUserService},
|
||||
infrastructure::{
|
||||
http::handlers::{
|
||||
delete_user_handler, get_me_handler, list_users_handler, update_me_handler,
|
||||
update_role_handler,
|
||||
},
|
||||
persistence::postgres_user_repository::PostgresUserRepository,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn qr_users_routes(pool: Arc<PgPool>, jwt: Arc<QrJwtService>) -> Router {
|
||||
let repo: Arc<dyn UserRepository> = Arc::new(PostgresUserRepository::new(pool.clone()));
|
||||
let service: Arc<dyn QrUserService> = Arc::new(QrUserServiceImpl::new(repo));
|
||||
|
||||
Router::new()
|
||||
.route("/users/me", get(get_me_handler).put(update_me_handler))
|
||||
.route("/users", get(list_users_handler))
|
||||
.route("/users/:id/role", put(update_role_handler))
|
||||
.route("/users/:id", delete(delete_user_handler))
|
||||
.layer(Extension(service))
|
||||
.layer(Extension(jwt.clone()))
|
||||
.layer(Extension(pool))
|
||||
.layer(from_fn(qr_auth_middleware))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http;
|
||||
pub mod persistence;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod postgres_user_repository;
|
||||
@@ -0,0 +1,74 @@
|
||||
use async_trait::async_trait;
|
||||
use imphnen_utils::errors::AppError;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::users::domain::{
|
||||
entity::{UpdateUserInput, UserEntity},
|
||||
repository::UserRepository,
|
||||
};
|
||||
|
||||
pub struct PostgresUserRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl PostgresUserRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserRepository for PostgresUserRepository {
|
||||
async fn find_by_id(&self, id: Uuid) -> Result<Option<UserEntity>, AppError> {
|
||||
sqlx::query_as::<_, UserEntity>(
|
||||
"SELECT id, email, name, role, provider, created_at, updated_at FROM users WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn find_all(&self) -> Result<Vec<UserEntity>, AppError> {
|
||||
sqlx::query_as::<_, UserEntity>(
|
||||
"SELECT id, email, name, role, provider, created_at, updated_at FROM users ORDER BY created_at DESC",
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn update(&self, id: Uuid, input: UpdateUserInput) -> Result<UserEntity, AppError> {
|
||||
sqlx::query_as::<_, UserEntity>(
|
||||
"UPDATE users SET name = COALESCE($1, name), email = COALESCE($2, email), updated_at = NOW() WHERE id = $3 RETURNING id, email, name, role, provider, created_at, updated_at",
|
||||
)
|
||||
.bind(input.name)
|
||||
.bind(input.email)
|
||||
.bind(id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn update_role(&self, id: Uuid, role: String) -> Result<UserEntity, AppError> {
|
||||
sqlx::query_as::<_, UserEntity>(
|
||||
"UPDATE users SET role = $1, updated_at = NOW() WHERE id = $2 RETURNING id, email, name, role, provider, created_at, updated_at",
|
||||
)
|
||||
.bind(role)
|
||||
.bind(id)
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("DELETE FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod domain;
|
||||
pub mod application;
|
||||
pub mod infrastructure;
|
||||
pub use infrastructure::http::routes::qr_users_routes;
|
||||
Reference in New Issue
Block a user