From 38106ecdb61dca7671231f7f9fb7ee162f7ade7c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 04:00:04 +0200 Subject: [PATCH] feat(presentation): all handlers --- crates/presentation/src/handlers/api_keys.rs | 19 ++++++ crates/presentation/src/handlers/feed.rs | 38 +++++++++++ .../src/handlers/notifications.rs | 18 ++++++ crates/presentation/src/handlers/social.rs | 51 +++++++++++++++ crates/presentation/src/handlers/thoughts.rs | 64 +++++++++++++++++++ 5 files changed, 190 insertions(+) diff --git a/crates/presentation/src/handlers/api_keys.rs b/crates/presentation/src/handlers/api_keys.rs index e69de29..ae42f3e 100644 --- a/crates/presentation/src/handlers/api_keys.rs +++ b/crates/presentation/src/handlers/api_keys.rs @@ -0,0 +1,19 @@ +use axum::{extract::{Path, State}, http::StatusCode, Json}; +use uuid::Uuid; +use api_types::{requests::CreateApiKeyRequest, responses::ApiKeyResponse}; +use application::use_cases::api_keys::{create_api_key, delete_api_key, list_api_keys}; +use domain::value_objects::ApiKeyId; +use crate::{errors::ApiError, extractors::AuthUser, state::AppState}; + +pub async fn get_api_keys(State(s): State, AuthUser(uid): AuthUser) -> Result>, ApiError> { + let keys = list_api_keys(&*s.api_keys, &uid).await?; + Ok(Json(keys.into_iter().map(|k| ApiKeyResponse { id: k.id.as_uuid(), name: k.name, created_at: k.created_at }).collect())) +} +pub async fn post_api_key(State(s): State, AuthUser(uid): AuthUser, Json(body): Json) -> Result, ApiError> { + let (key, raw) = create_api_key(&*s.api_keys, &uid, body.name).await?; + Ok(Json(serde_json::json!({ "id": key.id.as_uuid(), "name": key.name, "key": raw }))) +} +pub async fn delete_api_key_handler(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + delete_api_key(&*s.api_keys, &uid, &ApiKeyId::from_uuid(id)).await?; + Ok(StatusCode::NO_CONTENT) +} diff --git a/crates/presentation/src/handlers/feed.rs b/crates/presentation/src/handlers/feed.rs index e69de29..afe29ac 100644 --- a/crates/presentation/src/handlers/feed.rs +++ b/crates/presentation/src/handlers/feed.rs @@ -0,0 +1,38 @@ +use axum::{extract::{Path, Query, State}, Json}; +use api_types::requests::{PaginationQuery, SearchQuery}; +use application::use_cases::feed::{get_home_feed, get_public_feed, get_followers, get_following, search}; +use domain::models::feed::PageParams; +use crate::{errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState}; +use application::use_cases::profile::get_user_by_username; + +pub async fn home_feed(State(s): State, AuthUser(uid): AuthUser, Query(q): Query) -> Result, ApiError> { + let page = PageParams { page: q.page(), per_page: q.per_page() }; + let result = get_home_feed(&*s.feed, &*s.follows, &uid, page).await?; + Ok(Json(serde_json::json!({ "items": result.items.iter().map(|e| e.thought.id.as_uuid()).collect::>(), "total": result.total, "page": result.page }))) +} + +pub async fn public_feed(State(s): State, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query) -> Result, ApiError> { + let page = PageParams { page: q.page(), per_page: q.per_page() }; + let result = get_public_feed(&*s.feed, viewer.as_ref(), page).await?; + Ok(Json(serde_json::json!({ "items": result.items.iter().map(|e| e.thought.id.as_uuid()).collect::>(), "total": result.total, "page": result.page }))) +} + +pub async fn search_handler(State(s): State, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query) -> Result, ApiError> { + let page = PageParams { page: q.page.unwrap_or(1), per_page: q.per_page.unwrap_or(20) }; + let result = search(&*s.feed, &q.q, page, viewer.as_ref()).await?; + Ok(Json(serde_json::json!({ "items": result.items.iter().map(|e| e.thought.id.as_uuid()).collect::>(), "total": result.total }))) +} + +pub async fn get_following_handler(State(s): State, Path(username): Path, Query(q): Query) -> Result, ApiError> { + let user = get_user_by_username(&*s.users, &username).await?; + let page = PageParams { page: q.page(), per_page: q.per_page() }; + let result = get_following(&*s.follows, &user.id, page).await?; + Ok(Json(serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::>() }))) +} + +pub async fn get_followers_handler(State(s): State, Path(username): Path, Query(q): Query) -> Result, ApiError> { + let user = get_user_by_username(&*s.users, &username).await?; + let page = PageParams { page: q.page(), per_page: q.per_page() }; + let result = get_followers(&*s.follows, &user.id, page).await?; + Ok(Json(serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::>() }))) +} diff --git a/crates/presentation/src/handlers/notifications.rs b/crates/presentation/src/handlers/notifications.rs index e69de29..026a416 100644 --- a/crates/presentation/src/handlers/notifications.rs +++ b/crates/presentation/src/handlers/notifications.rs @@ -0,0 +1,18 @@ +use axum::{extract::{Path, State}, http::StatusCode, Json}; +use uuid::Uuid; +use domain::{models::feed::PageParams, value_objects::NotificationId}; +use crate::{errors::ApiError, extractors::AuthUser, state::AppState}; + +pub async fn list_notifications(State(s): State, AuthUser(uid): AuthUser) -> Result, ApiError> { + let page = PageParams { page: 1, per_page: 20 }; + let result = s.notifications.list_for_user(&uid, &page).await?; + Ok(Json(serde_json::json!({ "total": result.total, "unread": result.items.iter().filter(|n| !n.read).count() }))) +} +pub async fn mark_notification_read(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + s.notifications.mark_read(&NotificationId::from_uuid(id), &uid).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn mark_all_read(State(s): State, AuthUser(uid): AuthUser) -> Result { + s.notifications.mark_all_read(&uid).await?; + Ok(StatusCode::NO_CONTENT) +} diff --git a/crates/presentation/src/handlers/social.rs b/crates/presentation/src/handlers/social.rs index e69de29..6bef8ee 100644 --- a/crates/presentation/src/handlers/social.rs +++ b/crates/presentation/src/handlers/social.rs @@ -0,0 +1,51 @@ +use axum::{extract::{Path, State}, http::StatusCode, Json}; +use uuid::Uuid; +use api_types::requests::SetTopFriendsRequest; +use application::use_cases::social::*; +use application::use_cases::profile::{get_top_friends, set_top_friends, get_user_by_username}; +use domain::value_objects::{ThoughtId, UserId}; +use crate::{errors::ApiError, extractors::AuthUser, state::AppState}; + +pub async fn post_like(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + like_thought(&*s.likes, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn delete_like(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + unlike_thought(&*s.likes, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn post_boost(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + boost_thought(&*s.boosts, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn delete_boost(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + unboost_thought(&*s.boosts, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn post_follow(State(s): State, AuthUser(uid): AuthUser, Path(target): Path) -> Result { + follow_user(&*s.follows, &*s.events, &uid, &UserId::from_uuid(target)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn delete_follow(State(s): State, AuthUser(uid): AuthUser, Path(target): Path) -> Result { + unfollow_user(&*s.follows, &*s.events, &uid, &UserId::from_uuid(target)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn post_block(State(s): State, AuthUser(uid): AuthUser, Path(target): Path) -> Result { + block_user(&*s.blocks, &*s.events, &uid, &UserId::from_uuid(target)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn delete_block(State(s): State, AuthUser(uid): AuthUser, Path(target): Path) -> Result { + unblock_user(&*s.blocks, &uid, &UserId::from_uuid(target)).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn put_top_friends(State(s): State, AuthUser(uid): AuthUser, Json(body): Json) -> Result { + let ids: Vec = body.friend_ids.into_iter().map(UserId::from_uuid).collect(); + set_top_friends(&*s.top_friends, &uid, ids).await?; + Ok(StatusCode::NO_CONTENT) +} +pub async fn get_top_friends_handler(State(s): State, Path(username): Path) -> Result, ApiError> { + let user = get_user_by_username(&*s.users, &username).await?; + let friends = get_top_friends(&*s.top_friends, &user.id).await?; + let ids: Vec = friends.iter().map(|(tf, _)| tf.friend_id.as_uuid()).collect(); + Ok(Json(serde_json::json!({ "top_friends": ids }))) +} diff --git a/crates/presentation/src/handlers/thoughts.rs b/crates/presentation/src/handlers/thoughts.rs index e69de29..b267a81 100644 --- a/crates/presentation/src/handlers/thoughts.rs +++ b/crates/presentation/src/handlers/thoughts.rs @@ -0,0 +1,64 @@ +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, Json}; +use uuid::Uuid; +use api_types::requests::{CreateThoughtRequest, EditThoughtRequest}; +use application::use_cases::thoughts::{create_thought, delete_thought, edit_thought, get_thought, get_thread, CreateThoughtInput}; +use domain::value_objects::ThoughtId; +use crate::{errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState}; + +fn thought_to_json(t: &domain::models::thought::Thought, author: &domain::models::user::User, like_count: i64, boost_count: i64, reply_count: i64) -> serde_json::Value { + serde_json::json!({ + "id": t.id.as_uuid(), + "content": t.content.as_str(), + "author": to_user_response(author), + "in_reply_to_id": t.in_reply_to_id.as_ref().map(|x| x.as_uuid()), + "visibility": t.visibility.as_str(), + "content_warning": t.content_warning, + "sensitive": t.sensitive, + "like_count": like_count, + "boost_count": boost_count, + "reply_count": reply_count, + "created_at": t.created_at, + "updated_at": t.updated_at, + }) +} + +pub async fn post_thought(State(s): State, AuthUser(uid): AuthUser, Json(body): Json) -> Result { + let in_reply_to = body.in_reply_to_id.map(ThoughtId::from_uuid); + let out = create_thought(&*s.thoughts, &*s.users, &*s.events, CreateThoughtInput { + user_id: uid.clone(), + content: body.content, + in_reply_to_id: in_reply_to, + visibility: body.visibility, + content_warning: body.content_warning, + sensitive: body.sensitive.unwrap_or(false), + }).await?; + let author = s.users.find_by_id(&uid).await?.ok_or(domain::errors::DomainError::NotFound)?; + Ok((StatusCode::CREATED, Json(thought_to_json(&out.thought, &author, 0, 0, 0)))) +} + +pub async fn get_thought_handler(State(s): State, Path(id): Path, OptionalAuthUser(_viewer): OptionalAuthUser) -> Result, ApiError> { + let thought = get_thought(&*s.thoughts, &ThoughtId::from_uuid(id)).await?; + let author = s.users.find_by_id(&thought.user_id).await?.ok_or(domain::errors::DomainError::NotFound)?; + Ok(Json(thought_to_json(&thought, &author, 0, 0, 0))) +} + +pub async fn delete_thought_handler(State(s): State, AuthUser(uid): AuthUser, Path(id): Path) -> Result { + delete_thought(&*s.thoughts, &*s.events, &ThoughtId::from_uuid(id), &uid).await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn patch_thought(State(s): State, AuthUser(uid): AuthUser, Path(id): Path, Json(body): Json) -> Result { + edit_thought(&*s.thoughts, &*s.events, &ThoughtId::from_uuid(id), &uid, body.content).await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn get_thread_handler(State(s): State, Path(id): Path) -> Result>, ApiError> { + let thoughts = get_thread(&*s.thoughts, &ThoughtId::from_uuid(id)).await?; + let mut items = Vec::new(); + for t in &thoughts { + if let Ok(Some(author)) = s.users.find_by_id(&t.user_id).await { + items.push(thought_to_json(t, &author, 0, 0, 0)); + } + } + Ok(Json(items)) +}