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, Path(username): Path, OptionalAuthUser(viewer): OptionalAuthUser, ) -> Result, 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, 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))) } #[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, AuthUser(uid): AuthUser, ) -> Result, 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, AuthUser(uid): AuthUser, Query(q): Query, ) -> Result, 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::>(), }))) } pub async fn get_users( State(s): State, Query(params): Query>, ) -> Result, ApiError> { use domain::models::feed::PageParams; let page = params .get("page") .and_then(|v| v.parse::().ok()) .unwrap_or(1); let per_page = params .get("per_page") .and_then(|v| v.parse::().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, ) -> Result, ApiError> { let count = s.users.count().await?; Ok(Json(serde_json::json!({ "count": count }))) }