feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1
@@ -2,6 +2,7 @@ use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterRequest {
|
||||
/// Username (1-32 chars, alphanumeric + underscore)
|
||||
pub username: String,
|
||||
@@ -10,12 +11,14 @@ pub struct RegisterRequest {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateThoughtRequest {
|
||||
/// Up to 128 characters
|
||||
pub content: String,
|
||||
@@ -27,11 +30,13 @@ pub struct CreateThoughtRequest {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditThoughtRequest {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateProfileRequest {
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
@@ -41,12 +46,14 @@ pub struct UpdateProfileRequest {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetTopFriendsRequest {
|
||||
/// Ordered list of user UUIDs, max 8
|
||||
pub friend_ids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateApiKeyRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthResponse {
|
||||
pub token: String,
|
||||
pub user: UserResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserResponse {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
@@ -16,15 +18,20 @@ pub struct UserResponse {
|
||||
pub bio: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub header_url: Option<String>,
|
||||
pub custom_css: Option<String>,
|
||||
pub local: bool,
|
||||
pub is_followed_by_viewer: bool,
|
||||
#[serde(rename = "joinedAt")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ThoughtResponse {
|
||||
pub id: Uuid,
|
||||
pub content: String,
|
||||
pub author: UserResponse,
|
||||
#[serde(rename = "replyToId")]
|
||||
pub in_reply_to_id: Option<Uuid>,
|
||||
pub visibility: String,
|
||||
pub content_warning: Option<String>,
|
||||
@@ -39,6 +46,7 @@ pub struct ThoughtResponse {
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PagedResponse<T: Serialize + utoipa::ToSchema> {
|
||||
pub items: Vec<T>,
|
||||
pub total: i64,
|
||||
@@ -47,6 +55,7 @@ pub struct PagedResponse<T: Serialize + utoipa::ToSchema> {
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiKeyResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
@@ -54,6 +63,7 @@ pub struct ApiKeyResponse {
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotificationResponse {
|
||||
pub id: Uuid,
|
||||
pub notification_type: String,
|
||||
@@ -64,11 +74,13 @@ pub struct NotificationResponse {
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatedApiKeyResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
|
||||
@@ -14,7 +14,9 @@ pub fn to_user_response(u: &domain::models::user::User) -> UserResponse {
|
||||
bio: u.bio.clone(),
|
||||
avatar_url: u.avatar_url.clone(),
|
||||
header_url: u.header_url.clone(),
|
||||
custom_css: u.custom_css.clone(),
|
||||
local: u.local,
|
||||
is_followed_by_viewer: false,
|
||||
created_at: u.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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::{
|
||||
requests::UpdateProfileRequest,
|
||||
requests::{PaginationQuery, UpdateProfileRequest},
|
||||
responses::{ErrorResponse, UserResponse},
|
||||
};
|
||||
use application::use_cases::feed::list_users;
|
||||
@@ -24,9 +27,17 @@ use axum::{
|
||||
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?;
|
||||
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(
|
||||
@@ -81,6 +92,24 @@ pub async fn get_me(
|
||||
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>>,
|
||||
|
||||
@@ -14,7 +14,16 @@ pub fn router() -> Router<AppState> {
|
||||
// users — static paths before parameterised
|
||||
.route("/users", get(users::get_users))
|
||||
.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/{username}/top-friends",
|
||||
|
||||
Reference in New Issue
Block a user