Files
thoughts/crates/presentation/src/handlers/users.rs
Gabriel Kaszewski 8ef3a300bc
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 9m34s
test / unit (pull_request) Successful in 16m5s
test / integration (pull_request) Failing after 18m6s
fix(users): return camelCase from GET /users list — UserSummary was snake_case
2026-05-14 17:44:59 +02:00

173 lines
5.1 KiB
Rust

use crate::{
errors::ApiError,
extractors::{AuthUser, OptionalAuthUser},
handlers::auth::to_user_response,
state::AppState,
};
use api_types::{
requests::{PaginationQuery, 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 axum::{
extract::{Path, Query, State},
Json,
};
#[utoipa::path(
get, path = "/users/{username}",
params(("username" = String, Path, description = "Username")),
responses(
(status = 200, body = UserResponse),
(status = 404, description = "User not found", body = ErrorResponse),
)
)]
pub async fn get_user(
State(s): State<AppState>,
Path(username): Path<String>,
OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<UserResponse>, ApiError> {
let user = get_user_by_username(&*s.users, &username).await?;
let is_followed = if let Some(viewer_id) = viewer {
s.follows.find(&viewer_id, &user.id).await?.is_some()
} else {
false
};
let mut resp = to_user_response(&user);
resp.is_followed_by_viewer = is_followed;
Ok(Json(resp))
}
#[utoipa::path(
patch, path = "/users/me",
request_body = UpdateProfileRequest,
responses(
(status = 200, body = UserResponse),
(status = 401, description = "Unauthorized", body = ErrorResponse),
),
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)?;
Ok(Json(to_user_response(&user)))
}
#[utoipa::path(
get, path = "/users/me",
responses(
(status = 200, body = UserResponse),
(status = 401, description = "Unauthorized", body = ErrorResponse),
),
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)?;
Ok(Json(to_user_response(&user)))
}
pub async fn get_me_following_list(
State(s): State<AppState>,
AuthUser(uid): AuthUser,
Query(q): Query<PaginationQuery>,
) -> Result<Json<serde_json::Value>, ApiError> {
use application::use_cases::feed::get_following;
use domain::models::feed::PageParams;
let page = PageParams {
page: q.page(),
per_page: q.per_page(),
};
let result = get_following(&*s.follows, &uid, page).await?;
Ok(Json(serde_json::json!({
"total": result.total,
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>(),
})))
}
pub async fn get_users(
State(s): State<AppState>,
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 = 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(crate::handlers::auth::to_user_response)
.collect();
return Ok(Json(serde_json::json!({
"items": users, "total": result.total, "page": result.page, "per_page": result.per_page
})));
}
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,
"displayName": u.display_name,
"avatarUrl": u.avatar_url,
"bio": u.bio,
"headerUrl": null,
"customCss": null,
"local": true,
"isFollowedByViewer": false,
"joinedAt": null,
})
})
.collect();
Ok(Json(serde_json::json!({
"items": items, "total": total, "page": page, "per_page": per_page
})))
}
pub async fn get_user_count(
State(s): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
let count = s.users.count().await?;
Ok(Json(serde_json::json!({ "count": count })))
}