feat(presentation): handlers, OpenAPI/Scalar, routes, extractors
This commit is contained in:
250
Cargo.lock
generated
250
Cargo.lock
generated
@@ -114,12 +114,82 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"axum-macros",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-macros"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -605,6 +675,86 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.64"
|
version = "0.1.64"
|
||||||
@@ -848,6 +998,12 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -864,6 +1020,12 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1063,6 +1225,21 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "presentation"
|
name = "presentation"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"api-types",
|
||||||
|
"application",
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"domain",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-scalar",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
@@ -1260,6 +1437,17 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -1596,6 +1784,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@@ -1720,6 +1914,50 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@@ -1834,6 +2072,18 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-scalar"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "uuid", "chrono"
|
|||||||
jsonwebtoken = "9.3"
|
jsonwebtoken = "9.3"
|
||||||
bcrypt = "0.15"
|
bcrypt = "0.15"
|
||||||
utoipa = { version = "5.3", features = ["axum_extras", "uuid", "chrono"] }
|
utoipa = { version = "5.3", features = ["axum_extras", "uuid", "chrono"] }
|
||||||
utoipa-scalar = { version = "5.0", features = ["axum"] }
|
utoipa-scalar = { version = "0.3", features = ["axum"] }
|
||||||
domain = { path = "crates/domain" }
|
domain = { path = "crates/domain" }
|
||||||
application = { path = "crates/application" }
|
application = { path = "crates/application" }
|
||||||
api-types = { path = "crates/api-types" }
|
api-types = { path = "crates/api-types" }
|
||||||
|
|||||||
19
crates/presentation/Cargo.toml
Normal file
19
crates/presentation/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "presentation"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
domain = { workspace = true }
|
||||||
|
application = { workspace = true }
|
||||||
|
api-types = { path = "../api-types" }
|
||||||
|
axum = { workspace = true }
|
||||||
|
tower-http = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
utoipa = { workspace = true }
|
||||||
|
utoipa-scalar = { workspace = true }
|
||||||
25
crates/presentation/src/errors.rs
Normal file
25
crates/presentation/src/errors.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
|
||||||
|
use domain::errors::DomainError;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub struct AppError(DomainError);
|
||||||
|
|
||||||
|
impl From<DomainError> for AppError {
|
||||||
|
fn from(e: DomainError) -> Self { Self(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, message) = match &self.0 {
|
||||||
|
DomainError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
|
||||||
|
DomainError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
|
||||||
|
DomainError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
|
||||||
|
DomainError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg.clone()),
|
||||||
|
DomainError::Internal(msg) => {
|
||||||
|
tracing::error!("Internal error: {msg}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(status, Json(json!({ "error": message }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
38
crates/presentation/src/extractors/auth.rs
Normal file
38
crates/presentation/src/extractors/auth.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::{request::Parts, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use domain::value_objects::{Role, UserId};
|
||||||
|
use serde_json::json;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub struct JwtClaims {
|
||||||
|
pub user_id: UserId,
|
||||||
|
pub role: Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequestParts<AppState> for JwtClaims {
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||||
|
let auth_header = parts
|
||||||
|
.headers
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
(StatusCode::UNAUTHORIZED, Json(json!({ "error": "Missing Authorization header" }))).into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
|
||||||
|
(StatusCode::UNAUTHORIZED, Json(json!({ "error": "Invalid Authorization format" }))).into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (user_id, role) = state.token_issuer.verify(token).await.map_err(|_| {
|
||||||
|
(StatusCode::UNAUTHORIZED, Json(json!({ "error": "Invalid or expired token" }))).into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(JwtClaims { user_id, role })
|
||||||
|
}
|
||||||
|
}
|
||||||
28
crates/presentation/src/extractors/json.rs
Normal file
28
crates/presentation/src/extractors/json.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{rejection::JsonRejection, FromRequest, Request},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub struct ValidatedJson<T>(pub T);
|
||||||
|
|
||||||
|
impl<T, S> FromRequest<S> for ValidatedJson<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
S: Send + Sync,
|
||||||
|
Json<T>: FromRequest<S, Rejection = JsonRejection>,
|
||||||
|
{
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
Json::<T>::from_request(req, state)
|
||||||
|
.await
|
||||||
|
.map(|Json(value)| ValidatedJson(value))
|
||||||
|
.map_err(|rejection| {
|
||||||
|
(StatusCode::UNPROCESSABLE_ENTITY, Json(json!({ "error": rejection.body_text() }))).into_response()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/presentation/src/extractors/mod.rs
Normal file
5
crates/presentation/src/extractors/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod json;
|
||||||
|
|
||||||
|
pub use auth::JwtClaims;
|
||||||
|
pub use json::ValidatedJson;
|
||||||
56
crates/presentation/src/handlers/auth.rs
Normal file
56
crates/presentation/src/handlers/auth.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use axum::{extract::State, http::StatusCode, Json};
|
||||||
|
use api_types::{
|
||||||
|
requests::{LoginRequest, RegisterRequest},
|
||||||
|
responses::{AuthResponse, UserResponse},
|
||||||
|
};
|
||||||
|
use crate::{errors::AppError, extractors::{JwtClaims, ValidatedJson}, state::AppState};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post, path = "/api/v1/auth/register",
|
||||||
|
request_body = RegisterRequest,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "User registered", body = AuthResponse),
|
||||||
|
(status = 409, description = "Email already taken"),
|
||||||
|
(status = 422, description = "Validation error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn register(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
ValidatedJson(req): ValidatedJson<RegisterRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<AuthResponse>), AppError> {
|
||||||
|
let user = state.register_uc.execute(&req.email, &req.password).await?;
|
||||||
|
let token = state.token_issuer.issue(&user.id, &user.role).await.map_err(AppError::from)?;
|
||||||
|
Ok((StatusCode::CREATED, Json(AuthResponse { token, user: UserResponse::from_domain(&user) })))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post, path = "/api/v1/auth/login",
|
||||||
|
request_body = LoginRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Login successful", body = AuthResponse),
|
||||||
|
(status = 401, description = "Invalid credentials")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
ValidatedJson(req): ValidatedJson<LoginRequest>,
|
||||||
|
) -> Result<Json<AuthResponse>, AppError> {
|
||||||
|
let (user, token) = state.login_uc.execute(&req.email, &req.password).await?;
|
||||||
|
Ok(Json(AuthResponse { token, user: UserResponse::from_domain(&user) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get, path = "/api/v1/auth/me",
|
||||||
|
security(("bearer_token" = [])),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Current user profile", body = UserResponse),
|
||||||
|
(status = 401, description = "Unauthorized")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn me(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
claims: JwtClaims,
|
||||||
|
) -> Result<Json<UserResponse>, AppError> {
|
||||||
|
let user = state.get_profile_uc.execute(&claims.user_id).await?;
|
||||||
|
Ok(Json(UserResponse::from_domain(&user)))
|
||||||
|
}
|
||||||
7
crates/presentation/src/handlers/health.rs
Normal file
7
crates/presentation/src/handlers/health.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use axum::{http::StatusCode, Json};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[utoipa::path(get, path = "/health", responses((status = 200, description = "Service is healthy")))]
|
||||||
|
pub async fn health() -> (StatusCode, Json<serde_json::Value>) {
|
||||||
|
(StatusCode::OK, Json(json!({ "status": "ok" })))
|
||||||
|
}
|
||||||
2
crates/presentation/src/handlers/mod.rs
Normal file
2
crates/presentation/src/handlers/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod health;
|
||||||
6
crates/presentation/src/lib.rs
Normal file
6
crates/presentation/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod errors;
|
||||||
|
pub mod extractors;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod openapi;
|
||||||
|
pub mod routes;
|
||||||
|
pub mod state;
|
||||||
41
crates/presentation/src/openapi/mod.rs
Normal file
41
crates/presentation/src/openapi/mod.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use utoipa::{openapi::security::{Http, HttpAuthScheme, SecurityScheme}, Modify, OpenApi};
|
||||||
|
use utoipa_scalar::{Scalar, Servable};
|
||||||
|
use axum::Router;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::health::health,
|
||||||
|
crate::handlers::auth::register,
|
||||||
|
crate::handlers::auth::login,
|
||||||
|
crate::handlers::auth::me,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
api_types::requests::RegisterRequest,
|
||||||
|
api_types::requests::LoginRequest,
|
||||||
|
api_types::responses::AuthResponse,
|
||||||
|
api_types::responses::UserResponse,
|
||||||
|
)),
|
||||||
|
modifiers(&SecurityAddon),
|
||||||
|
info(title = "k-template", version = "0.1.0")
|
||||||
|
)]
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
struct SecurityAddon;
|
||||||
|
impl Modify for SecurityAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
|
components.add_security_scheme(
|
||||||
|
"bearer_token",
|
||||||
|
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn openapi_router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
|
||||||
|
.route("/api-docs/openapi.json", axum::routing::get(|| async { axum::Json(ApiDoc::openapi()) }))
|
||||||
|
}
|
||||||
16
crates/presentation/src/routes.rs
Normal file
16
crates/presentation/src/routes.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use axum::{routing::{get, post}, Router};
|
||||||
|
use crate::{handlers::{auth, health}, openapi::openapi_router, state::AppState};
|
||||||
|
|
||||||
|
pub fn api_v1_router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/auth/register", post(auth::register))
|
||||||
|
.route("/auth/login", post(auth::login))
|
||||||
|
.route("/auth/me", get(auth::me))
|
||||||
|
.route("/health", get(health::health))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.nest("/api/v1", api_v1_router())
|
||||||
|
.merge(openapi_router())
|
||||||
|
}
|
||||||
22
crates/presentation/src/state.rs
Normal file
22
crates/presentation/src/state.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use application::use_cases::{GetProfile, LoginUser, RegisterUser};
|
||||||
|
use domain::ports::TokenIssuer;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub register_uc: Arc<RegisterUser>,
|
||||||
|
pub login_uc: Arc<LoginUser>,
|
||||||
|
pub get_profile_uc: Arc<GetProfile>,
|
||||||
|
pub token_issuer: Arc<dyn TokenIssuer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(
|
||||||
|
register_uc: Arc<RegisterUser>,
|
||||||
|
login_uc: Arc<LoginUser>,
|
||||||
|
get_profile_uc: Arc<GetProfile>,
|
||||||
|
token_issuer: Arc<dyn TokenIssuer>,
|
||||||
|
) -> Self {
|
||||||
|
Self { register_uc, login_uc, get_profile_uc, token_issuer }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user