Refactor handlers and OpenAPI documentation for improved readability and consistency
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 6m49s
test / unit (pull_request) Successful in 16m24s
test / integration (pull_request) Failing after 17m7s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 6m49s
test / unit (pull_request) Successful in 16m24s
test / integration (pull_request) Failing after 17m7s
- Reorganized imports in health, notifications, social, thoughts, and users handlers for clarity. - Updated function signatures in handlers to improve readability by aligning parameters. - Enhanced JSON response formatting in notifications and thoughts handlers. - Improved error handling in user-related functions. - Refactored OpenAPI documentation to maintain consistent formatting and structure. - Cleaned up unnecessary code and comments across various files. - Ensured consistent use of `Arc` for shared state in AppState and WorkerHandlers.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
|
||||
use domain::errors::DomainError;
|
||||
use api_types::responses::ErrorResponse;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
|
||||
pub enum ApiError {
|
||||
Domain(DomainError),
|
||||
@@ -9,20 +13,27 @@ pub enum ApiError {
|
||||
}
|
||||
|
||||
impl From<DomainError> for ApiError {
|
||||
fn from(e: DomainError) -> Self { Self::Domain(e) }
|
||||
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(_)) => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".into()),
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
|
||||
Self::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
|
||||
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(_)) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"internal server error".into(),
|
||||
),
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
|
||||
Self::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
|
||||
};
|
||||
(status, Json(ErrorResponse { error: msg })).into_response()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{errors::ApiError, state::AppState};
|
||||
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<UserId>);
|
||||
@@ -8,7 +8,8 @@ pub struct OptionalAuthUser(pub Option<UserId>);
|
||||
impl FromRequestParts<AppState> for AuthUser {
|
||||
type Rejection = ApiError;
|
||||
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, ApiError> {
|
||||
extract_user_id(parts, state).await?
|
||||
extract_user_id(parts, state)
|
||||
.await?
|
||||
.ok_or(ApiError::Unauthorized)
|
||||
.map(AuthUser)
|
||||
}
|
||||
@@ -25,7 +26,11 @@ async fn extract_user_id(parts: &mut Parts, state: &AppState) -> Result<Option<U
|
||||
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);
|
||||
return state
|
||||
.auth
|
||||
.validate_token(token)
|
||||
.map(Some)
|
||||
.map_err(|_| ApiError::Unauthorized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,50 @@
|
||||
use axum::{extract::{Path, State}, http::StatusCode, Json};
|
||||
use uuid::Uuid;
|
||||
use api_types::{requests::CreateApiKeyRequest, responses::{ApiKeyResponse, CreatedApiKeyResponse}};
|
||||
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};
|
||||
use api_types::{
|
||||
requests::CreateApiKeyRequest,
|
||||
responses::{ApiKeyResponse, CreatedApiKeyResponse},
|
||||
};
|
||||
use application::use_cases::api_keys::{create_api_key, delete_api_key, list_api_keys};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use domain::value_objects::ApiKeyId;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(get, path = "/api-keys", responses((status = 200, description = "API keys", body = Vec<ApiKeyResponse>)), security(("bearer_auth" = [])))]
|
||||
pub async fn get_api_keys(State(s): State<AppState>, AuthUser(uid): AuthUser) -> Result<Json<Vec<ApiKeyResponse>>, ApiError> {
|
||||
pub async fn get_api_keys(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<Json<Vec<ApiKeyResponse>>, 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()))
|
||||
Ok(Json(
|
||||
keys.into_iter()
|
||||
.map(|k| ApiKeyResponse {
|
||||
id: k.id.as_uuid(),
|
||||
name: k.name,
|
||||
created_at: k.created_at,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
#[utoipa::path(post, path = "/api-keys", request_body = CreateApiKeyRequest, responses((status = 200, description = "Created — raw key shown once", body = CreatedApiKeyResponse)), security(("bearer_auth" = [])))]
|
||||
pub async fn post_api_key(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<CreateApiKeyRequest>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
pub async fn post_api_key(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<CreateApiKeyRequest>,
|
||||
) -> Result<Json<serde_json::Value>, 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 })))
|
||||
Ok(Json(
|
||||
serde_json::json!({ "id": key.id.as_uuid(), "name": key.name, "key": raw }),
|
||||
))
|
||||
}
|
||||
#[utoipa::path(delete, path = "/api-keys/{id}", params(("id" = uuid::Uuid, Path, description = "Key ID")), responses((status = 204, description = "Deleted")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_api_key_handler(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn delete_api_key_handler(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
delete_api_key(&*s.api_keys, &uid, &ApiKeyId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use api_types::{requests::{LoginRequest, RegisterRequest}, responses::{AuthResponse, ErrorResponse, UserResponse}};
|
||||
use application::use_cases::auth::{login, register, LoginInput, RegisterInput};
|
||||
use crate::{errors::ApiError, state::AppState};
|
||||
use api_types::{
|
||||
requests::{LoginRequest, RegisterRequest},
|
||||
responses::{AuthResponse, ErrorResponse, UserResponse},
|
||||
};
|
||||
use application::use_cases::auth::{login, register, LoginInput, RegisterInput};
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
|
||||
pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
|
||||
UserResponse {
|
||||
@@ -25,13 +28,26 @@ pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
|
||||
(status = 422, description = "Invalid input", body = ErrorResponse),
|
||||
)
|
||||
)]
|
||||
pub async fn post_register(State(s): State<AppState>, Json(body): Json<RegisterRequest>) -> Result<impl IntoResponse, ApiError> {
|
||||
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) };
|
||||
pub async fn post_register(
|
||||
State(s): State<AppState>,
|
||||
Json(body): Json<RegisterRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -43,10 +59,22 @@ pub async fn post_register(State(s): State<AppState>, Json(body): Json<RegisterR
|
||||
(status = 401, description = "Invalid credentials", body = ErrorResponse),
|
||||
)
|
||||
)]
|
||||
pub async fn post_login(State(s): State<AppState>, Json(body): Json<LoginRequest>) -> Result<impl IntoResponse, ApiError> {
|
||||
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) }))
|
||||
pub async fn post_login(
|
||||
State(s): State<AppState>,
|
||||
Json(body): Json<LoginRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
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),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
use axum::{extract::{Path, Query, State}, Json};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, OptionalAuthUser},
|
||||
handlers::auth::to_user_response,
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::requests::{PaginationQuery, SearchQuery};
|
||||
use api_types::responses::ThoughtResponse;
|
||||
use application::use_cases::feed::{get_home_feed, get_public_feed, get_followers, get_following, get_user_feed, get_by_tag, get_popular_tags as uc_get_popular_tags};
|
||||
use application::use_cases::search::{search_thoughts, search_users};
|
||||
use domain::models::feed::PageParams;
|
||||
use crate::{errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState};
|
||||
use application::use_cases::feed::{
|
||||
get_by_tag, get_followers, get_following, get_home_feed,
|
||||
get_popular_tags as uc_get_popular_tags, get_public_feed, get_user_feed,
|
||||
};
|
||||
use application::use_cases::profile::get_user_by_username;
|
||||
use application::use_cases::search::{search_thoughts, search_users};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
use domain::models::feed::PageParams;
|
||||
|
||||
fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtResponse {
|
||||
ThoughtResponse {
|
||||
@@ -32,8 +43,15 @@ fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtResponse {
|
||||
responses((status = 200, description = "Home feed")),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn home_feed(State(s): State<AppState>, AuthUser(uid): AuthUser, Query(q): Query<PaginationQuery>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
pub async fn home_feed(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, 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(to_thought_response).collect::<Vec<_>>(),
|
||||
@@ -48,8 +66,15 @@ pub async fn home_feed(State(s): State<AppState>, AuthUser(uid): AuthUser, Query
|
||||
params(PaginationQuery),
|
||||
responses((status = 200, description = "Public feed"))
|
||||
)]
|
||||
pub async fn public_feed(State(s): State<AppState>, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query<PaginationQuery>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
pub async fn public_feed(
|
||||
State(s): State<AppState>,
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, 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(to_thought_response).collect::<Vec<_>>(),
|
||||
@@ -69,25 +94,53 @@ pub async fn search_handler(
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<SearchQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams { page: q.page.unwrap_or(1), per_page: q.per_page.unwrap_or(20) };
|
||||
let page = PageParams {
|
||||
page: q.page.unwrap_or(1),
|
||||
per_page: q.per_page.unwrap_or(20),
|
||||
};
|
||||
let query = q.q.trim().to_string();
|
||||
|
||||
let (thoughts_result, users_result) = tokio::join!(
|
||||
search_thoughts(&*s.search, &query, PageParams { page: page.page, per_page: page.per_page }, viewer.as_ref()),
|
||||
search_users(&*s.search, &query, PageParams { page: page.page, per_page: page.per_page }),
|
||||
search_thoughts(
|
||||
&*s.search,
|
||||
&query,
|
||||
PageParams {
|
||||
page: page.page,
|
||||
per_page: page.per_page
|
||||
},
|
||||
viewer.as_ref()
|
||||
),
|
||||
search_users(
|
||||
&*s.search,
|
||||
&query,
|
||||
PageParams {
|
||||
page: page.page,
|
||||
per_page: page.per_page
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
let thoughts = thoughts_result?.items.into_iter().map(|e| serde_json::json!({
|
||||
"id": e.thought.id.as_uuid(),
|
||||
"content": e.thought.content.as_str(),
|
||||
"author": to_user_response(&e.author),
|
||||
"like_count": e.like_count,
|
||||
"boost_count": e.boost_count,
|
||||
"reply_count": e.reply_count,
|
||||
"created_at": e.thought.created_at,
|
||||
})).collect::<Vec<_>>();
|
||||
let thoughts = thoughts_result?
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
serde_json::json!({
|
||||
"id": e.thought.id.as_uuid(),
|
||||
"content": e.thought.content.as_str(),
|
||||
"author": to_user_response(&e.author),
|
||||
"like_count": e.like_count,
|
||||
"boost_count": e.boost_count,
|
||||
"reply_count": e.reply_count,
|
||||
"created_at": e.thought.created_at,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let users = users_result?.items.into_iter().map(|u| to_user_response(&u)).collect::<Vec<_>>();
|
||||
let users = users_result?
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|u| to_user_response(&u))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"query": query,
|
||||
@@ -96,18 +149,36 @@ pub async fn search_handler(
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn get_following_handler(State(s): State<AppState>, Path(username): Path<String>, Query(q): Query<PaginationQuery>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
pub async fn get_following_handler(
|
||||
State(s): State<AppState>,
|
||||
Path(username): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
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::<Vec<_>>() })))
|
||||
Ok(Json(
|
||||
serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::<Vec<_>>() }),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_followers_handler(State(s): State<AppState>, Path(username): Path<String>, Query(q): Query<PaginationQuery>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
pub async fn get_followers_handler(
|
||||
State(s): State<AppState>,
|
||||
Path(username): Path<String>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
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::<Vec<_>>() })))
|
||||
Ok(Json(
|
||||
serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::<Vec<_>>() }),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -125,7 +196,10 @@ pub async fn user_thoughts_handler(
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
let page = PageParams {
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_user_feed(&*s.feed, &user.id, page, viewer.as_ref()).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"total": result.total,
|
||||
@@ -139,7 +213,10 @@ pub async fn get_popular_tags(
|
||||
State(s): State<AppState>,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let limit: usize = params.get("limit").and_then(|v| v.parse().ok()).unwrap_or(20);
|
||||
let limit: usize = params
|
||||
.get("limit")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(20);
|
||||
let tags = uc_get_popular_tags(&*s.tags, limit.min(100)).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"tags": tags.iter().map(|(name, count)| serde_json::json!({
|
||||
@@ -163,7 +240,10 @@ pub async fn tag_thoughts_handler(
|
||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams { page: q.page(), per_page: q.per_page() };
|
||||
let page = PageParams {
|
||||
page: q.page(),
|
||||
per_page: q.per_page(),
|
||||
};
|
||||
let result = get_by_tag(&*s.feed, &tag_name, page, viewer.as_ref()).await?;
|
||||
Ok(Json(serde_json::json!({
|
||||
"tag": tag_name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::{extract::State, Json};
|
||||
use crate::state::AppState;
|
||||
use axum::{extract::State, Json};
|
||||
|
||||
#[utoipa::path(get, path = "/health", responses((status = 200, description = "Service health status")))]
|
||||
pub async fn health_handler(State(s): State<AppState>) -> Json<serde_json::Value> {
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
use axum::{extract::{Path, State}, http::StatusCode, Json};
|
||||
use uuid::Uuid;
|
||||
use domain::{models::feed::PageParams, value_objects::NotificationId};
|
||||
use application::use_cases::notifications::{
|
||||
list_notifications as uc_list_notifications,
|
||||
mark_notification_read as uc_mark_notification_read,
|
||||
mark_all_notifications_read,
|
||||
};
|
||||
use crate::{errors::ApiError, extractors::AuthUser, state::AppState};
|
||||
use application::use_cases::notifications::{
|
||||
list_notifications as uc_list_notifications, mark_all_notifications_read,
|
||||
mark_notification_read as uc_mark_notification_read,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use domain::{models::feed::PageParams, value_objects::NotificationId};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(get, path = "/notifications", responses((status = 200, description = "Notification summary")), security(("bearer_auth" = [])))]
|
||||
pub async fn list_notifications(State(s): State<AppState>, AuthUser(uid): AuthUser) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams { page: 1, per_page: 20 };
|
||||
pub async fn list_notifications(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let page = PageParams {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
};
|
||||
let result = uc_list_notifications(&*s.notifications, &uid, page).await?;
|
||||
Ok(Json(serde_json::json!({ "total": result.total, "unread": result.items.iter().filter(|n| !n.read).count() })))
|
||||
Ok(Json(
|
||||
serde_json::json!({ "total": result.total, "unread": result.items.iter().filter(|n| !n.read).count() }),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/notifications/{id}/read", params(("id" = uuid::Uuid, Path, description = "Notification ID")), responses((status = 204, description = "Marked read")), security(("bearer_auth" = [])))]
|
||||
pub async fn mark_notification_read(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn mark_notification_read(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
uc_mark_notification_read(&*s.notifications, &NotificationId::from_uuid(id), &uid).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/notifications/read-all", responses((status = 204, description = "All marked read")), security(("bearer_auth" = [])))]
|
||||
pub async fn mark_all_read(State(s): State<AppState>, AuthUser(uid): AuthUser) -> Result<StatusCode, ApiError> {
|
||||
pub async fn mark_all_read(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
mark_all_notifications_read(&*s.notifications, &uid).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -1,61 +1,107 @@
|
||||
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};
|
||||
use api_types::requests::SetTopFriendsRequest;
|
||||
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
|
||||
use application::use_cases::social::*;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use domain::value_objects::{ThoughtId, UserId};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(post, path = "/thoughts/{id}/like", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Liked")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_like(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn post_like(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
like_thought(&*s.likes, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/thoughts/{id}/like", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Unliked")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_like(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn delete_like(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unlike_thought(&*s.likes, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(post, path = "/thoughts/{id}/boost", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Boosted")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_boost(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn post_boost(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
boost_thought(&*s.boosts, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/thoughts/{id}/boost", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses((status = 204, description = "Unboosted")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_boost(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn delete_boost(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unboost_thought(&*s.boosts, &*s.events, &uid, &ThoughtId::from_uuid(id)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(post, path = "/users/{id}/follow", params(("id" = uuid::Uuid, Path, description = "User ID")), responses((status = 204, description = "Following")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_follow(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(target): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn post_follow(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(target): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
follow_user(&*s.follows, &*s.events, &uid, &UserId::from_uuid(target)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/users/{id}/follow", params(("id" = uuid::Uuid, Path, description = "User ID")), responses((status = 204, description = "Unfollowed")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_follow(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(target): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn delete_follow(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(target): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unfollow_user(&*s.follows, &*s.events, &uid, &UserId::from_uuid(target)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(post, path = "/users/{id}/block", params(("id" = uuid::Uuid, Path, description = "User ID")), responses((status = 204, description = "Blocked")), security(("bearer_auth" = [])))]
|
||||
pub async fn post_block(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(target): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn post_block(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(target): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
block_user(&*s.blocks, &*s.events, &uid, &UserId::from_uuid(target)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(delete, path = "/users/{id}/block", params(("id" = uuid::Uuid, Path, description = "User ID")), responses((status = 204, description = "Unblocked")), security(("bearer_auth" = [])))]
|
||||
pub async fn delete_block(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(target): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn delete_block(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(target): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
unblock_user(&*s.blocks, &*s.events, &uid, &UserId::from_uuid(target)).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(put, path = "/users/me/top-friends", request_body = SetTopFriendsRequest, responses((status = 204, description = "Top friends updated")), security(("bearer_auth" = [])))]
|
||||
pub async fn put_top_friends(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<SetTopFriendsRequest>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn put_top_friends(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<SetTopFriendsRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let ids: Vec<UserId> = body.friend_ids.into_iter().map(UserId::from_uuid).collect();
|
||||
set_top_friends(&*s.top_friends, &uid, ids).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
#[utoipa::path(get, path = "/users/{username}/top-friends", params(("username" = String, Path, description = "Username")), responses((status = 200, description = "Top friends list")))]
|
||||
pub async fn get_top_friends_handler(State(s): State<AppState>, Path(username): Path<String>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
pub async fn get_top_friends_handler(
|
||||
State(s): State<AppState>,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, 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<Uuid> = friends.iter().map(|(tf, _)| tf.friend_id.as_uuid()).collect();
|
||||
let ids: Vec<Uuid> = friends
|
||||
.iter()
|
||||
.map(|(tf, _)| tf.friend_id.as_uuid())
|
||||
.collect();
|
||||
Ok(Json(serde_json::json!({ "top_friends": ids })))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, Json};
|
||||
use uuid::Uuid;
|
||||
use api_types::{requests::{CreateThoughtRequest, EditThoughtRequest}, responses::ErrorResponse};
|
||||
use application::use_cases::thoughts::{create_thought, delete_thought, edit_thought, get_thought, get_thread, CreateThoughtInput};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::{AuthUser, OptionalAuthUser},
|
||||
handlers::auth::to_user_response,
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::{
|
||||
requests::{CreateThoughtRequest, EditThoughtRequest},
|
||||
responses::ErrorResponse,
|
||||
};
|
||||
use application::use_cases::thoughts::{
|
||||
create_thought, delete_thought, edit_thought, get_thought, get_thread, CreateThoughtInput,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use domain::value_objects::ThoughtId;
|
||||
use crate::{errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState};
|
||||
use uuid::Uuid;
|
||||
|
||||
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 {
|
||||
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(),
|
||||
@@ -32,18 +53,35 @@ fn thought_to_json(t: &domain::models::thought::Thought, author: &domain::models
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn post_thought(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<CreateThoughtRequest>) -> Result<impl IntoResponse, ApiError> {
|
||||
pub async fn post_thought(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<CreateThoughtRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
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))))
|
||||
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)),
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -54,9 +92,17 @@ pub async fn post_thought(State(s): State<AppState>, AuthUser(uid): AuthUser, Js
|
||||
(status = 404, description = "Not found", body = ErrorResponse),
|
||||
)
|
||||
)]
|
||||
pub async fn get_thought_handler(State(s): State<AppState>, Path(id): Path<Uuid>, OptionalAuthUser(_viewer): OptionalAuthUser) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
pub async fn get_thought_handler(
|
||||
State(s): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
OptionalAuthUser(_viewer): OptionalAuthUser,
|
||||
) -> Result<Json<serde_json::Value>, 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)?;
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -70,7 +116,11 @@ pub async fn get_thought_handler(State(s): State<AppState>, Path(id): Path<Uuid>
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn delete_thought_handler(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>) -> Result<StatusCode, ApiError> {
|
||||
pub async fn delete_thought_handler(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
delete_thought(&*s.thoughts, &*s.events, &ThoughtId::from_uuid(id), &uid).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -86,8 +136,20 @@ pub async fn delete_thought_handler(State(s): State<AppState>, AuthUser(uid): Au
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn patch_thought(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(id): Path<Uuid>, Json(body): Json<EditThoughtRequest>) -> Result<StatusCode, ApiError> {
|
||||
edit_thought(&*s.thoughts, &*s.events, &ThoughtId::from_uuid(id), &uid, body.content).await?;
|
||||
pub async fn patch_thought(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<EditThoughtRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
edit_thought(
|
||||
&*s.thoughts,
|
||||
&*s.events,
|
||||
&ThoughtId::from_uuid(id),
|
||||
&uid,
|
||||
body.content,
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -98,7 +160,10 @@ pub async fn patch_thought(State(s): State<AppState>, AuthUser(uid): AuthUser, P
|
||||
(status = 200, description = "Thread (root + replies)"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_thread_handler(State(s): State<AppState>, Path(id): Path<Uuid>) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
|
||||
pub async fn get_thread_handler(
|
||||
State(s): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<serde_json::Value>>, ApiError> {
|
||||
let thoughts = get_thread(&*s.thoughts, &ThoughtId::from_uuid(id)).await?;
|
||||
let mut items = Vec::new();
|
||||
for t in &thoughts {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use axum::{extract::{Path, Query, State}, Json};
|
||||
use api_types::{requests::UpdateProfileRequest, responses::{ErrorResponse, UserResponse}};
|
||||
use crate::{
|
||||
errors::ApiError, extractors::AuthUser, handlers::auth::to_user_response, state::AppState,
|
||||
};
|
||||
use api_types::{
|
||||
requests::UpdateProfileRequest,
|
||||
responses::{ErrorResponse, UserResponse},
|
||||
};
|
||||
use application::use_cases::feed::list_users;
|
||||
use application::use_cases::profile::{get_user_by_username, update_profile};
|
||||
use application::use_cases::search::search_users;
|
||||
use application::use_cases::feed::list_users;
|
||||
use crate::{errors::ApiError, extractors::AuthUser, handlers::auth::to_user_response, state::AppState};
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/users/{username}",
|
||||
@@ -13,7 +21,10 @@ use crate::{errors::ApiError, extractors::AuthUser, handlers::auth::to_user_resp
|
||||
(status = 404, description = "User not found", body = ErrorResponse),
|
||||
)
|
||||
)]
|
||||
pub async fn get_user(State(s): State<AppState>, Path(username): Path<String>) -> Result<Json<UserResponse>, ApiError> {
|
||||
pub async fn get_user(
|
||||
State(s): State<AppState>,
|
||||
Path(username): Path<String>,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user = get_user_by_username(&*s.users, &username).await?;
|
||||
Ok(Json(to_user_response(&user)))
|
||||
}
|
||||
@@ -27,9 +38,26 @@ pub async fn get_user(State(s): State<AppState>, Path(username): Path<String>) -
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn patch_profile(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<UpdateProfileRequest>) -> Result<Json<UserResponse>, 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)?;
|
||||
pub async fn patch_profile(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
Json(body): Json<UpdateProfileRequest>,
|
||||
) -> Result<Json<UserResponse>, 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)))
|
||||
}
|
||||
|
||||
@@ -41,8 +69,15 @@ pub async fn patch_profile(State(s): State<AppState>, AuthUser(uid): AuthUser, J
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_me(State(s): State<AppState>, AuthUser(uid): AuthUser) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user = s.users.find_by_id(&uid).await?.ok_or(domain::errors::DomainError::NotFound)?;
|
||||
pub async fn get_me(
|
||||
State(s): State<AppState>,
|
||||
AuthUser(uid): AuthUser,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user = s
|
||||
.users
|
||||
.find_by_id(&uid)
|
||||
.await?
|
||||
.ok_or(domain::errors::DomainError::NotFound)?;
|
||||
Ok(Json(to_user_response(&user)))
|
||||
}
|
||||
|
||||
@@ -51,13 +86,23 @@ pub async fn get_users(
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
use domain::models::feed::PageParams;
|
||||
let page = params.get("page").and_then(|v| v.parse::<u64>().ok()).unwrap_or(1);
|
||||
let per_page = params.get("per_page").and_then(|v| v.parse::<u64>().ok()).unwrap_or(20);
|
||||
let page = params
|
||||
.get("page")
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(1);
|
||||
let per_page = params
|
||||
.get("per_page")
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(20);
|
||||
let page_params = PageParams { page, per_page };
|
||||
|
||||
if let Some(q) = params.get("q").filter(|q| !q.trim().is_empty()) {
|
||||
let result = search_users(&*s.search, q, page_params).await?;
|
||||
let users: Vec<_> = result.items.iter().map(|u| crate::handlers::auth::to_user_response(u)).collect();
|
||||
let users: Vec<_> = result
|
||||
.items
|
||||
.iter()
|
||||
.map(|u| crate::handlers::auth::to_user_response(u))
|
||||
.collect();
|
||||
return Ok(Json(serde_json::json!({
|
||||
"items": users, "total": result.total, "page": result.page, "per_page": result.per_page
|
||||
})));
|
||||
@@ -66,18 +111,22 @@ pub async fn get_users(
|
||||
let all = list_users(&*s.users).await?;
|
||||
let total = all.len() as i64;
|
||||
let start = ((page - 1) * per_page) as usize;
|
||||
let items: Vec<_> = all.into_iter()
|
||||
.skip(start).take(per_page as usize)
|
||||
.map(|u| serde_json::json!({
|
||||
"id": u.id.as_uuid(),
|
||||
"username": u.username,
|
||||
"display_name": u.display_name,
|
||||
"avatar_url": u.avatar_url,
|
||||
"bio": u.bio,
|
||||
"thought_count": u.thought_count,
|
||||
"follower_count": u.follower_count,
|
||||
"following_count": u.following_count,
|
||||
}))
|
||||
let items: Vec<_> = all
|
||||
.into_iter()
|
||||
.skip(start)
|
||||
.take(per_page as usize)
|
||||
.map(|u| {
|
||||
serde_json::json!({
|
||||
"id": u.id.as_uuid(),
|
||||
"username": u.username,
|
||||
"display_name": u.display_name,
|
||||
"avatar_url": u.avatar_url,
|
||||
"bio": u.bio,
|
||||
"thought_count": u.thought_count,
|
||||
"follower_count": u.follower_count,
|
||||
"following_count": u.following_count,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(serde_json::json!({
|
||||
"items": items, "total": total, "page": page, "per_page": per_page
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use api_types::{
|
||||
requests::CreateApiKeyRequest,
|
||||
responses::{ApiKeyResponse, CreatedApiKeyResponse},
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
use api_types::{requests::CreateApiKeyRequest, responses::{ApiKeyResponse, CreatedApiKeyResponse}};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use api_types::{
|
||||
requests::{LoginRequest, RegisterRequest},
|
||||
responses::{AuthResponse, ErrorResponse},
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
use api_types::{requests::{LoginRequest, RegisterRequest}, responses::{AuthResponse, ErrorResponse}};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(crate::handlers::auth::post_register, crate::handlers::auth::post_login),
|
||||
paths(
|
||||
crate::handlers::auth::post_register,
|
||||
crate::handlers::auth::post_login
|
||||
),
|
||||
components(schemas(RegisterRequest, LoginRequest, AuthResponse, ErrorResponse))
|
||||
)]
|
||||
pub struct AuthDoc;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::handlers::feed::home_feed,
|
||||
crate::handlers::feed::public_feed,
|
||||
crate::handlers::feed::search_handler,
|
||||
crate::handlers::feed::user_thoughts_handler,
|
||||
crate::handlers::feed::tag_thoughts_handler,
|
||||
),
|
||||
)]
|
||||
#[openapi(paths(
|
||||
crate::handlers::feed::home_feed,
|
||||
crate::handlers::feed::public_feed,
|
||||
crate::handlers::feed::search_handler,
|
||||
crate::handlers::feed::user_thoughts_handler,
|
||||
crate::handlers::feed::tag_thoughts_handler,
|
||||
))]
|
||||
pub struct FeedDoc;
|
||||
|
||||
@@ -9,8 +9,8 @@ mod users;
|
||||
|
||||
use axum::Router;
|
||||
use utoipa::{
|
||||
Modify, OpenApi,
|
||||
openapi::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme, SecurityScheme},
|
||||
Modify, OpenApi,
|
||||
};
|
||||
use utoipa_scalar::{Scalar, Servable};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use utoipa::OpenApi;
|
||||
use api_types::requests::SetTopFriendsRequest;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use api_types::{
|
||||
requests::{CreateThoughtRequest, EditThoughtRequest},
|
||||
responses::ErrorResponse,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
use api_types::{requests::{CreateThoughtRequest, EditThoughtRequest}, responses::ErrorResponse};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use api_types::{
|
||||
requests::UpdateProfileRequest,
|
||||
responses::{ErrorResponse, UserResponse},
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
use api_types::{requests::UpdateProfileRequest, responses::{UserResponse, ErrorResponse}};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{handlers::*, openapi, state::AppState};
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use crate::{handlers::*, openapi, state::AppState};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
let api_routes = Router::new()
|
||||
@@ -16,7 +16,10 @@ pub fn router() -> Router<AppState> {
|
||||
.route("/users/count", get(users::get_user_count))
|
||||
.route("/users/me", get(users::get_me).patch(users::patch_profile))
|
||||
.route("/users/me/top-friends", put(social::put_top_friends))
|
||||
.route("/users/{username}/top-friends", get(social::get_top_friends_handler))
|
||||
.route(
|
||||
"/users/{username}/top-friends",
|
||||
get(social::get_top_friends_handler),
|
||||
)
|
||||
// follows & blocks (use {id} param)
|
||||
.route(
|
||||
"/users/{id}/follow",
|
||||
@@ -48,15 +51,30 @@ pub fn router() -> Router<AppState> {
|
||||
.route("/feed", get(feed::home_feed))
|
||||
.route("/feed/public", get(feed::public_feed))
|
||||
.route("/search", get(feed::search_handler))
|
||||
.route("/users/{username}/follower-list", get(feed::get_followers_handler))
|
||||
.route("/users/{username}/following-list", get(feed::get_following_handler))
|
||||
.route("/users/{username}/thoughts", get(feed::user_thoughts_handler))
|
||||
.route(
|
||||
"/users/{username}/follower-list",
|
||||
get(feed::get_followers_handler),
|
||||
)
|
||||
.route(
|
||||
"/users/{username}/following-list",
|
||||
get(feed::get_following_handler),
|
||||
)
|
||||
.route(
|
||||
"/users/{username}/thoughts",
|
||||
get(feed::user_thoughts_handler),
|
||||
)
|
||||
.route("/tags/popular", get(feed::get_popular_tags))
|
||||
.route("/tags/{name}", get(feed::tag_thoughts_handler))
|
||||
// notifications
|
||||
.route("/notifications", get(notifications::list_notifications))
|
||||
.route("/notifications/read-all", post(notifications::mark_all_read))
|
||||
.route("/notifications/{id}/read", post(notifications::mark_notification_read))
|
||||
.route(
|
||||
"/notifications/read-all",
|
||||
post(notifications::mark_all_read),
|
||||
)
|
||||
.route(
|
||||
"/notifications/{id}/read",
|
||||
post(notifications::mark_notification_read),
|
||||
)
|
||||
// api keys
|
||||
.route(
|
||||
"/api-keys",
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
use domain::ports::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub thoughts: Arc<dyn ThoughtRepository>,
|
||||
pub likes: Arc<dyn LikeRepository>,
|
||||
pub boosts: Arc<dyn BoostRepository>,
|
||||
pub follows: Arc<dyn FollowRepository>,
|
||||
pub blocks: Arc<dyn BlockRepository>,
|
||||
pub tags: Arc<dyn TagRepository>,
|
||||
pub api_keys: Arc<dyn ApiKeyRepository>,
|
||||
pub top_friends: Arc<dyn TopFriendRepository>,
|
||||
pub users: Arc<dyn UserRepository>,
|
||||
pub thoughts: Arc<dyn ThoughtRepository>,
|
||||
pub likes: Arc<dyn LikeRepository>,
|
||||
pub boosts: Arc<dyn BoostRepository>,
|
||||
pub follows: Arc<dyn FollowRepository>,
|
||||
pub blocks: Arc<dyn BlockRepository>,
|
||||
pub tags: Arc<dyn TagRepository>,
|
||||
pub api_keys: Arc<dyn ApiKeyRepository>,
|
||||
pub top_friends: Arc<dyn TopFriendRepository>,
|
||||
pub notifications: Arc<dyn NotificationRepository>,
|
||||
pub remote_actors: Arc<dyn RemoteActorRepository>,
|
||||
pub feed: Arc<dyn FeedRepository>,
|
||||
pub search: Arc<dyn SearchPort>,
|
||||
pub auth: Arc<dyn AuthService>,
|
||||
pub hasher: Arc<dyn PasswordHasher>,
|
||||
pub events: Arc<dyn EventPublisher>,
|
||||
pub feed: Arc<dyn FeedRepository>,
|
||||
pub search: Arc<dyn SearchPort>,
|
||||
pub auth: Arc<dyn AuthService>,
|
||||
pub hasher: Arc<dyn PasswordHasher>,
|
||||
pub events: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user