blog/backend/feature/post/src/adapter/delivery/post_controller.rs
SquidSpirit e6b41a768f
All checks were successful
Frontend CI / build (push) Successful in 1m13s
Deployment / deployment (release) Successful in 6m59s
BLOG-119 Restricted access to unpublished posts (#124)
### Description

This PR introduces an authorization layer for the post feature. It ensures that create, update, and read operations for posts are properly controlled based on user authentication status and post visibility (published vs. unpublished).

#### Key Changes:

* **Restricted Access to Unpublished Posts**:
    * Unauthenticated users can no longer access unpublished posts via the `GET /post/{id}` endpoint. Attempting to do so will now result in an `HTTP 401 Unauthorized` error.
    * The `get_all_post_info` endpoint is now aware of the user's authentication status to correctly filter posts.

* **Authentication Required for Modifications**:
    * Creating (`POST /post`) and updating (`PUT /post/{id}`) posts now requires an authenticated user. The `user_id` is passed from the web handler through the controller to the use cases.

* **New Error Type**:
    * A new `PostError::Unauthorized` variant has been added to handle access control failures gracefully.

* **API & Core Logic Updates**:
    * The `PostController`, use cases (`GetFullPostUseCase`, `GetAllPostInfoUseCase`, etc.), and web handlers have been updated to accept and process the `user_id`.
    * The `GetFullPostUseCase` now contains the primary logic to prevent unauthenticated access to draft posts.
    * OpenAPI (Utopia) documentation has been updated to reflect these new authorization rules.

### Package Changes

_No response_

### Screenshots

_No response_

### Reference

Resolves #119

### Checklist

- [x] A milestone is set
- [x] The related issuse has been linked to this branch

Reviewed-on: #124
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
2025-08-06 22:13:54 +08:00

204 lines
5.9 KiB
Rust

use std::sync::Arc;
use async_trait::async_trait;
use crate::{
adapter::delivery::{
create_label_request_dto::CreateLabelRequestDto,
create_post_request_dto::CreatePostRequestDto, post_info_query_dto::PostQueryDto,
update_label_request_dto::UpdateLabelRequestDto,
update_post_request_dto::UpdatePostRequestDto,
},
application::{
error::post_error::PostError,
use_case::{
create_label_use_case::CreateLabelUseCase, create_post_use_case::CreatePostUseCase,
get_all_labels_use_case::GetAllLabelsUseCase,
get_all_post_info_use_case::GetAllPostInfoUseCase,
get_full_post_use_case::GetFullPostUseCase, update_label_use_case::UpdateLabelUseCase,
update_post_use_case::UpdatePostUseCase,
},
},
};
use super::{
label_response_dto::LabelResponseDto, post_info_response_dto::PostInfoResponseDto,
post_response_dto::PostResponseDto,
};
#[async_trait]
pub trait PostController: Send + Sync {
async fn get_all_post_info(
&self,
query: PostQueryDto,
user_id: Option<i32>,
) -> Result<Vec<PostInfoResponseDto>, PostError>;
async fn get_post_by_id(
&self,
id: i32,
user_id: Option<i32>,
) -> Result<PostResponseDto, PostError>;
async fn create_post(
&self,
post: CreatePostRequestDto,
user_id: i32,
) -> Result<PostResponseDto, PostError>;
async fn update_post(
&self,
id: i32,
post: UpdatePostRequestDto,
user_id: i32,
) -> Result<PostResponseDto, PostError>;
async fn create_label(
&self,
label: CreateLabelRequestDto,
) -> Result<LabelResponseDto, PostError>;
async fn update_label(
&self,
id: i32,
label: UpdateLabelRequestDto,
) -> Result<LabelResponseDto, PostError>;
async fn get_all_labels(&self) -> Result<Vec<LabelResponseDto>, PostError>;
}
pub struct PostControllerImpl {
get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
create_post_use_case: Arc<dyn CreatePostUseCase>,
update_post_use_case: Arc<dyn UpdatePostUseCase>,
create_label_use_case: Arc<dyn CreateLabelUseCase>,
update_label_use_case: Arc<dyn UpdateLabelUseCase>,
get_all_labels_use_case: Arc<dyn GetAllLabelsUseCase>,
}
impl PostControllerImpl {
pub fn new(
get_all_post_info_use_case: Arc<dyn GetAllPostInfoUseCase>,
get_full_post_use_case: Arc<dyn GetFullPostUseCase>,
create_post_use_case: Arc<dyn CreatePostUseCase>,
update_post_use_case: Arc<dyn UpdatePostUseCase>,
create_label_use_case: Arc<dyn CreateLabelUseCase>,
update_label_use_case: Arc<dyn UpdateLabelUseCase>,
get_all_labels_use_case: Arc<dyn GetAllLabelsUseCase>,
) -> Self {
Self {
get_all_post_info_use_case,
get_full_post_use_case,
create_post_use_case,
update_post_use_case,
create_label_use_case,
update_label_use_case,
get_all_labels_use_case,
}
}
}
#[async_trait]
impl PostController for PostControllerImpl {
async fn get_all_post_info(
&self,
query: PostQueryDto,
user_id: Option<i32>,
) -> Result<Vec<PostInfoResponseDto>, PostError> {
let result = self
.get_all_post_info_use_case
.execute(query.is_published_only.unwrap_or(true), user_id)
.await;
result.map(|post_info_list| {
let post_info_response_dto_list: Vec<PostInfoResponseDto> = post_info_list
.into_iter()
.map(|post_info| PostInfoResponseDto::from(post_info))
.collect();
post_info_response_dto_list
})
}
async fn get_post_by_id(
&self,
id: i32,
user_id: Option<i32>,
) -> Result<PostResponseDto, PostError> {
let result = self.get_full_post_use_case.execute(id, user_id).await;
result.map(PostResponseDto::from)
}
async fn create_label(
&self,
label: CreateLabelRequestDto,
) -> Result<LabelResponseDto, PostError> {
let mut label_entity = label.into_entity();
let id = self
.create_label_use_case
.execute(label_entity.clone())
.await?;
label_entity.id = id;
Ok(LabelResponseDto::from(label_entity))
}
async fn update_label(
&self,
id: i32,
label: UpdateLabelRequestDto,
) -> Result<LabelResponseDto, PostError> {
let label_entity = label.into_entity(id);
self.update_label_use_case
.execute(label_entity.clone())
.await?;
Ok(LabelResponseDto::from(label_entity))
}
async fn get_all_labels(&self) -> Result<Vec<LabelResponseDto>, PostError> {
let result = self.get_all_labels_use_case.execute().await;
result.map(|labels| {
labels
.into_iter()
.map(|label| LabelResponseDto::from(label))
.collect()
})
}
async fn create_post(
&self,
post: CreatePostRequestDto,
user_id: i32,
) -> Result<PostResponseDto, PostError> {
let label_ids = post.label_ids.clone();
let post_entity = post.into_entity();
let id = self
.create_post_use_case
.execute(post_entity, &label_ids)
.await?;
self.get_post_by_id(id, Some(user_id)).await
}
async fn update_post(
&self,
id: i32,
post: UpdatePostRequestDto,
user_id: i32,
) -> Result<PostResponseDto, PostError> {
let label_ids = post.label_ids.clone();
let post_entity = post.into_entity(id);
self.update_post_use_case
.execute(post_entity, &label_ids)
.await?;
self.get_post_by_id(id, Some(user_id)).await
}
}