feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1

Merged
GKaszewski merged 334 commits from v2 into master 2026-05-16 09:42:43 +00:00
5 changed files with 63 additions and 4 deletions
Showing only changes of commit aadd876994 - Show all commits

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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>>,

View File

@@ -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",