From fb39ea24693f5542f25e2deb206407d2ae7d25ed Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 03:56:42 +0200 Subject: [PATCH] feat(presentation): state, errors, extractors, auth and user handlers --- crates/presentation/src/errors.rs | 29 ++++++++++++ crates/presentation/src/extractors.rs | 47 +++++++++++++++++++ crates/presentation/src/handlers/api_keys.rs | 0 crates/presentation/src/handlers/auth.rs | 35 ++++++++++++++ crates/presentation/src/handlers/feed.rs | 0 crates/presentation/src/handlers/mod.rs | 7 +++ .../src/handlers/notifications.rs | 0 crates/presentation/src/handlers/social.rs | 0 crates/presentation/src/handlers/thoughts.rs | 0 crates/presentation/src/handlers/users.rs | 15 ++++++ crates/presentation/src/lib.rs | 4 ++ crates/presentation/src/state.rs | 21 +++++++++ 12 files changed, 158 insertions(+) create mode 100644 crates/presentation/src/errors.rs create mode 100644 crates/presentation/src/extractors.rs create mode 100644 crates/presentation/src/handlers/api_keys.rs create mode 100644 crates/presentation/src/handlers/auth.rs create mode 100644 crates/presentation/src/handlers/feed.rs create mode 100644 crates/presentation/src/handlers/mod.rs create mode 100644 crates/presentation/src/handlers/notifications.rs create mode 100644 crates/presentation/src/handlers/social.rs create mode 100644 crates/presentation/src/handlers/thoughts.rs create mode 100644 crates/presentation/src/handlers/users.rs create mode 100644 crates/presentation/src/lib.rs create mode 100644 crates/presentation/src/state.rs diff --git a/crates/presentation/src/errors.rs b/crates/presentation/src/errors.rs new file mode 100644 index 0000000..e239872 --- /dev/null +++ b/crates/presentation/src/errors.rs @@ -0,0 +1,29 @@ +use axum::{http::StatusCode, response::{IntoResponse, Response}, Json}; +use domain::errors::DomainError; +use api_types::responses::ErrorResponse; + +pub enum ApiError { + Domain(DomainError), + Unauthorized, + BadRequest(String), +} + +impl From for ApiError { + fn from(e: DomainError) -> Self { Self::Domain(e) } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let (status, msg) = match self { + Self::Domain(DomainError::NotFound) => (StatusCode::NOT_FOUND, "not found".into()), + Self::Domain(DomainError::Unauthorized) => (StatusCode::UNAUTHORIZED, "unauthorized".into()), + Self::Domain(DomainError::Forbidden) => (StatusCode::FORBIDDEN, "forbidden".into()), + Self::Domain(DomainError::Conflict(m)) => (StatusCode::CONFLICT, m), + Self::Domain(DomainError::InvalidInput(m)) => (StatusCode::UNPROCESSABLE_ENTITY, m), + Self::Domain(DomainError::Internal(m)) => (StatusCode::INTERNAL_SERVER_ERROR, m), + Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()), + Self::BadRequest(m) => (StatusCode::BAD_REQUEST, m), + }; + (status, Json(ErrorResponse { error: msg })).into_response() + } +} diff --git a/crates/presentation/src/extractors.rs b/crates/presentation/src/extractors.rs new file mode 100644 index 0000000..fc7b04e --- /dev/null +++ b/crates/presentation/src/extractors.rs @@ -0,0 +1,47 @@ +use axum::{extract::FromRequestParts, http::request::Parts}; +use domain::value_objects::UserId; +use crate::{errors::ApiError, state::AppState}; + +pub struct AuthUser(pub UserId); +pub struct OptionalAuthUser(pub Option); + +impl FromRequestParts for AuthUser { + type Rejection = ApiError; + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result { + extract_user_id(parts, state).await? + .ok_or(ApiError::Unauthorized) + .map(AuthUser) + } +} + +impl FromRequestParts for OptionalAuthUser { + type Rejection = ApiError; + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result { + Ok(OptionalAuthUser(extract_user_id(parts, state).await?)) + } +} + +async fn extract_user_id(parts: &mut Parts, state: &AppState) -> Result, ApiError> { + if let Some(auth_header) = parts.headers.get("Authorization") { + if let Ok(s) = auth_header.to_str() { + if let Some(token) = s.strip_prefix("Bearer ") { + return state.auth.validate_token(token).map(Some).map_err(|_| ApiError::Unauthorized); + } + } + } + if let Some(key_header) = parts.headers.get("X-Api-Key") { + if let Ok(raw) = key_header.to_str() { + let hash = sha256_hex(raw); + if let Ok(Some(key)) = state.api_keys.find_by_hash(&hash).await { + return Ok(Some(key.user_id)); + } + } + } + Ok(None) +} + +fn sha256_hex(s: &str) -> String { + use sha2::{Digest, Sha256}; + let hash = Sha256::digest(s.as_bytes()); + hex::encode(hash) +} diff --git a/crates/presentation/src/handlers/api_keys.rs b/crates/presentation/src/handlers/api_keys.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/presentation/src/handlers/auth.rs b/crates/presentation/src/handlers/auth.rs new file mode 100644 index 0000000..a549ca1 --- /dev/null +++ b/crates/presentation/src/handlers/auth.rs @@ -0,0 +1,35 @@ +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use api_types::{requests::{LoginRequest, RegisterRequest}, responses::{AuthResponse, UserResponse}}; +use application::use_cases::auth::{login, register, LoginInput, RegisterInput}; +use crate::{errors::ApiError, state::AppState}; + +pub fn to_user_response(u: &domain::models::user::User) -> UserResponse { + UserResponse { + id: u.id.as_uuid(), + username: u.username.to_string(), + display_name: u.display_name.clone(), + bio: u.bio.clone(), + avatar_url: u.avatar_url.clone(), + header_url: u.header_url.clone(), + local: u.local, + created_at: u.created_at, + } +} + +pub async fn post_register(State(s): State, Json(body): Json) -> Result { + let out = register(&*s.users, &*s.hasher, &*s.auth, &*s.events, RegisterInput { + username: body.username, + email: body.email, + password: body.password, + }).await?; + let resp = AuthResponse { token: out.token, user: to_user_response(&out.user) }; + Ok((StatusCode::CREATED, Json(resp))) +} + +pub async fn post_login(State(s): State, Json(body): Json) -> Result { + let out = login(&*s.users, &*s.hasher, &*s.auth, LoginInput { + email: body.email, + password: body.password, + }).await?; + Ok(Json(AuthResponse { token: out.token, user: to_user_response(&out.user) })) +} diff --git a/crates/presentation/src/handlers/feed.rs b/crates/presentation/src/handlers/feed.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/presentation/src/handlers/mod.rs b/crates/presentation/src/handlers/mod.rs new file mode 100644 index 0000000..02c578f --- /dev/null +++ b/crates/presentation/src/handlers/mod.rs @@ -0,0 +1,7 @@ +pub mod api_keys; +pub mod auth; +pub mod feed; +pub mod notifications; +pub mod social; +pub mod thoughts; +pub mod users; diff --git a/crates/presentation/src/handlers/notifications.rs b/crates/presentation/src/handlers/notifications.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/presentation/src/handlers/social.rs b/crates/presentation/src/handlers/social.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/presentation/src/handlers/thoughts.rs b/crates/presentation/src/handlers/thoughts.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/presentation/src/handlers/users.rs b/crates/presentation/src/handlers/users.rs new file mode 100644 index 0000000..43532ba --- /dev/null +++ b/crates/presentation/src/handlers/users.rs @@ -0,0 +1,15 @@ +use axum::{extract::{Path, State}, Json}; +use api_types::{requests::UpdateProfileRequest, responses::UserResponse}; +use application::use_cases::profile::{get_user_by_username, update_profile}; +use crate::{errors::ApiError, extractors::AuthUser, handlers::auth::to_user_response, state::AppState}; + +pub async fn get_user(State(s): State, Path(username): Path) -> Result, ApiError> { + let user = get_user_by_username(&*s.users, &username).await?; + Ok(Json(to_user_response(&user))) +} + +pub async fn patch_profile(State(s): State, AuthUser(uid): AuthUser, Json(body): Json) -> Result, ApiError> { + update_profile(&*s.users, &uid, body.display_name, body.bio, body.avatar_url, body.header_url, body.custom_css).await?; + let user = s.users.find_by_id(&uid).await?.ok_or(domain::errors::DomainError::NotFound)?; + Ok(Json(to_user_response(&user))) +} diff --git a/crates/presentation/src/lib.rs b/crates/presentation/src/lib.rs new file mode 100644 index 0000000..e53c3f9 --- /dev/null +++ b/crates/presentation/src/lib.rs @@ -0,0 +1,4 @@ +pub mod errors; +pub mod extractors; +pub mod handlers; +pub mod state; diff --git a/crates/presentation/src/state.rs b/crates/presentation/src/state.rs new file mode 100644 index 0000000..c615716 --- /dev/null +++ b/crates/presentation/src/state.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; +use domain::ports::*; + +#[derive(Clone)] +pub struct AppState { + pub users: Arc, + pub thoughts: Arc, + pub likes: Arc, + pub boosts: Arc, + pub follows: Arc, + pub blocks: Arc, + pub tags: Arc, + pub api_keys: Arc, + pub top_friends: Arc, + pub notifications: Arc, + pub remote_actors: Arc, + pub feed: Arc, + pub auth: Arc, + pub hasher: Arc, + pub events: Arc, +}