feat: camelCase JSON responses, isFollowedByViewer, customCss, GET /users/me/following-list
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 9m15s
test / unit (pull_request) Successful in 16m3s
test / integration (pull_request) Failing after 17m19s

This commit is contained in:
2026-05-14 17:04:42 +02:00
parent d3b7ecad15
commit aadd876994
5 changed files with 63 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RegisterRequest { pub struct RegisterRequest {
/// Username (1-32 chars, alphanumeric + underscore) /// Username (1-32 chars, alphanumeric + underscore)
pub username: String, pub username: String,
@@ -10,12 +11,14 @@ pub struct RegisterRequest {
} }
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoginRequest { pub struct LoginRequest {
pub email: String, pub email: String,
pub password: String, pub password: String,
} }
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateThoughtRequest { pub struct CreateThoughtRequest {
/// Up to 128 characters /// Up to 128 characters
pub content: String, pub content: String,
@@ -27,11 +30,13 @@ pub struct CreateThoughtRequest {
} }
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct EditThoughtRequest { pub struct EditThoughtRequest {
pub content: String, pub content: String,
} }
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateProfileRequest { pub struct UpdateProfileRequest {
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@@ -41,12 +46,14 @@ pub struct UpdateProfileRequest {
} }
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SetTopFriendsRequest { pub struct SetTopFriendsRequest {
/// Ordered list of user UUIDs, max 8 /// Ordered list of user UUIDs, max 8
pub friend_ids: Vec<Uuid>, pub friend_ids: Vec<Uuid>,
} }
#[derive(Deserialize, utoipa::ToSchema)] #[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateApiKeyRequest { pub struct CreateApiKeyRequest {
pub name: String, pub name: String,
} }

View File

@@ -3,12 +3,14 @@ use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
#[derive(Serialize, utoipa::ToSchema)] #[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct AuthResponse { pub struct AuthResponse {
pub token: String, pub token: String,
pub user: UserResponse, pub user: UserResponse,
} }
#[derive(Serialize, Clone, utoipa::ToSchema)] #[derive(Serialize, Clone, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UserResponse { pub struct UserResponse {
pub id: Uuid, pub id: Uuid,
pub username: String, pub username: String,
@@ -16,15 +18,20 @@ pub struct UserResponse {
pub bio: Option<String>, pub bio: Option<String>,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
pub header_url: Option<String>, pub header_url: Option<String>,
pub custom_css: Option<String>,
pub local: bool, pub local: bool,
pub is_followed_by_viewer: bool,
#[serde(rename = "joinedAt")]
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
#[derive(Serialize, Clone, utoipa::ToSchema)] #[derive(Serialize, Clone, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ThoughtResponse { pub struct ThoughtResponse {
pub id: Uuid, pub id: Uuid,
pub content: String, pub content: String,
pub author: UserResponse, pub author: UserResponse,
#[serde(rename = "replyToId")]
pub in_reply_to_id: Option<Uuid>, pub in_reply_to_id: Option<Uuid>,
pub visibility: String, pub visibility: String,
pub content_warning: Option<String>, pub content_warning: Option<String>,
@@ -39,6 +46,7 @@ pub struct ThoughtResponse {
} }
#[derive(Serialize, utoipa::ToSchema)] #[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct PagedResponse<T: Serialize + utoipa::ToSchema> { pub struct PagedResponse<T: Serialize + utoipa::ToSchema> {
pub items: Vec<T>, pub items: Vec<T>,
pub total: i64, pub total: i64,
@@ -47,6 +55,7 @@ pub struct PagedResponse<T: Serialize + utoipa::ToSchema> {
} }
#[derive(Serialize, utoipa::ToSchema)] #[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeyResponse { pub struct ApiKeyResponse {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
@@ -54,6 +63,7 @@ pub struct ApiKeyResponse {
} }
#[derive(Serialize, utoipa::ToSchema)] #[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct NotificationResponse { pub struct NotificationResponse {
pub id: Uuid, pub id: Uuid,
pub notification_type: String, pub notification_type: String,
@@ -64,11 +74,13 @@ pub struct NotificationResponse {
} }
#[derive(Serialize, utoipa::ToSchema)] #[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ErrorResponse { pub struct ErrorResponse {
pub error: String, pub error: String,
} }
#[derive(Serialize, utoipa::ToSchema)] #[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreatedApiKeyResponse { pub struct CreatedApiKeyResponse {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,

View File

@@ -14,7 +14,9 @@ pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
bio: u.bio.clone(), bio: u.bio.clone(),
avatar_url: u.avatar_url.clone(), avatar_url: u.avatar_url.clone(),
header_url: u.header_url.clone(), header_url: u.header_url.clone(),
custom_css: u.custom_css.clone(),
local: u.local, local: u.local,
is_followed_by_viewer: false,
created_at: u.created_at, created_at: u.created_at,
} }
} }

View File

@@ -1,8 +1,11 @@
use crate::{ use crate::{
errors::ApiError, extractors::AuthUser, handlers::auth::to_user_response, state::AppState, errors::ApiError,
extractors::{AuthUser, OptionalAuthUser},
handlers::auth::to_user_response,
state::AppState,
}; };
use api_types::{ use api_types::{
requests::UpdateProfileRequest, requests::{PaginationQuery, UpdateProfileRequest},
responses::{ErrorResponse, UserResponse}, responses::{ErrorResponse, UserResponse},
}; };
use application::use_cases::feed::list_users; use application::use_cases::feed::list_users;
@@ -24,9 +27,17 @@ use axum::{
pub async fn get_user( pub async fn get_user(
State(s): State<AppState>, State(s): State<AppState>,
Path(username): Path<String>, Path(username): Path<String>,
OptionalAuthUser(viewer): OptionalAuthUser,
) -> Result<Json<UserResponse>, ApiError> { ) -> Result<Json<UserResponse>, ApiError> {
let user = get_user_by_username(&*s.users, &username).await?; let user = get_user_by_username(&*s.users, &username).await?;
Ok(Json(to_user_response(&user))) 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( #[utoipa::path(
@@ -81,6 +92,24 @@ pub async fn get_me(
Ok(Json(to_user_response(&user))) 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( pub async fn get_users(
State(s): State<AppState>, State(s): State<AppState>,
Query(params): Query<std::collections::HashMap<String, String>>, Query(params): Query<std::collections::HashMap<String, String>>,

View File

@@ -14,7 +14,16 @@ pub fn router() -> Router<AppState> {
// users — static paths before parameterised // users — static paths before parameterised
.route("/users", get(users::get_users)) .route("/users", get(users::get_users))
.route("/users/count", get(users::get_user_count)) .route("/users/count", get(users::get_user_count))
.route("/users/me", get(users::get_me).patch(users::patch_profile)) .route(
"/users/me",
get(users::get_me)
.patch(users::patch_profile)
.put(users::patch_profile),
)
.route(
"/users/me/following-list",
get(users::get_me_following_list),
)
.route("/users/me/top-friends", put(social::put_top_friends)) .route("/users/me/top-friends", put(social::put_top_friends))
.route( .route(
"/users/{username}/top-friends", "/users/{username}/top-friends",