feat: implement user follow/unfollow functionality and thought retrieval by user

- Added follow and unfollow endpoints for users.
- Implemented logic to retrieve thoughts by a specific user.
- Updated user error handling to include cases for already following and not following.
- Created persistence layer for follow relationships.
- Enhanced user and thought schemas to support new features.
- Added tests for follow/unfollow endpoints and thought retrieval.
- Updated frontend to display thoughts and allow posting new thoughts.
This commit is contained in:
2025-09-05 19:08:37 +02:00
parent 912259ef54
commit decf81e535
31 changed files with 872 additions and 155 deletions

View File

@@ -0,0 +1,87 @@
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, post},
Router,
};
use app::{
error::UserError,
persistence::thought::{create_thought, delete_thought, get_thought},
state::AppState,
};
use models::{params::thought::CreateThoughtParams, schemas::thought::ThoughtSchema};
use crate::{
error::ApiError,
extractor::{AuthUser, Json, Valid},
models::{ApiErrorResponse, ParamsErrorResponse},
};
#[utoipa::path(
post,
path = "/thoughts",
request_body = CreateThoughtParams,
responses(
(status = 201, description = "Thought created", body = ThoughtSchema),
(status = 400, description = "Bad request", body = ApiErrorResponse),
(status = 422, description = "Validation error", body = ParamsErrorResponse)
),
security(
("api_key" = []),
("bearer_auth" = [])
)
)]
async fn thoughts_post(
State(state): State<AppState>,
auth_user: AuthUser,
Valid(Json(params)): Valid<Json<CreateThoughtParams>>,
) -> Result<impl IntoResponse, ApiError> {
let thought = create_thought(&state.conn, auth_user.id, params).await?;
let author = app::persistence::user::get_user(&state.conn, auth_user.id)
.await?
.ok_or(UserError::NotFound)?; // Should not happen if auth is valid
let schema = ThoughtSchema::from_models(&thought, &author);
Ok((StatusCode::CREATED, Json(schema)))
}
#[utoipa::path(
delete,
path = "/thoughts/{id}",
params(
("id" = i32, Path, description = "Thought ID")
),
responses(
(status = 204, description = "Thought deleted"),
(status = 403, description = "Forbidden", body = ApiErrorResponse),
(status = 404, description = "Not Found", body = ApiErrorResponse)
),
security(
("api_key" = []),
("bearer_auth" = [])
)
)]
async fn thoughts_delete(
State(state): State<AppState>,
auth_user: AuthUser,
Path(id): Path<i32>,
) -> Result<impl IntoResponse, ApiError> {
let thought = get_thought(&state.conn, id)
.await?
.ok_or(UserError::NotFound)?;
if thought.author_id != auth_user.id {
return Err(UserError::Forbidden.into());
}
delete_thought(&state.conn, id).await?;
Ok(StatusCode::NO_CONTENT)
}
pub fn create_thought_router() -> Router<AppState> {
Router::new()
.route("/", post(thoughts_post))
.route("/{id}", delete(thoughts_delete))
}