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>
This commit is contained in:
parent
a9df43943e
commit
e6b41a768f
@ -31,16 +31,26 @@ pub trait PostController: Send + Sync {
|
|||||||
async fn get_all_post_info(
|
async fn get_all_post_info(
|
||||||
&self,
|
&self,
|
||||||
query: PostQueryDto,
|
query: PostQueryDto,
|
||||||
|
user_id: Option<i32>,
|
||||||
) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
||||||
|
|
||||||
async fn get_post_by_id(&self, id: i32) -> Result<PostResponseDto, PostError>;
|
async fn get_post_by_id(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
user_id: Option<i32>,
|
||||||
|
) -> Result<PostResponseDto, PostError>;
|
||||||
|
|
||||||
async fn create_post(&self, post: CreatePostRequestDto) -> Result<PostResponseDto, PostError>;
|
async fn create_post(
|
||||||
|
&self,
|
||||||
|
post: CreatePostRequestDto,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<PostResponseDto, PostError>;
|
||||||
|
|
||||||
async fn update_post(
|
async fn update_post(
|
||||||
&self,
|
&self,
|
||||||
id: i32,
|
id: i32,
|
||||||
post: UpdatePostRequestDto,
|
post: UpdatePostRequestDto,
|
||||||
|
user_id: i32,
|
||||||
) -> Result<PostResponseDto, PostError>;
|
) -> Result<PostResponseDto, PostError>;
|
||||||
|
|
||||||
async fn create_label(
|
async fn create_label(
|
||||||
@ -94,10 +104,11 @@ impl PostController for PostControllerImpl {
|
|||||||
async fn get_all_post_info(
|
async fn get_all_post_info(
|
||||||
&self,
|
&self,
|
||||||
query: PostQueryDto,
|
query: PostQueryDto,
|
||||||
|
user_id: Option<i32>,
|
||||||
) -> Result<Vec<PostInfoResponseDto>, PostError> {
|
) -> Result<Vec<PostInfoResponseDto>, PostError> {
|
||||||
let result = self
|
let result = self
|
||||||
.get_all_post_info_use_case
|
.get_all_post_info_use_case
|
||||||
.execute(query.is_published_only.unwrap_or(true))
|
.execute(query.is_published_only.unwrap_or(true), user_id)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
result.map(|post_info_list| {
|
result.map(|post_info_list| {
|
||||||
@ -110,8 +121,12 @@ impl PostController for PostControllerImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_post_by_id(&self, id: i32) -> Result<PostResponseDto, PostError> {
|
async fn get_post_by_id(
|
||||||
let result = self.get_full_post_use_case.execute(id).await;
|
&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)
|
result.map(PostResponseDto::from)
|
||||||
}
|
}
|
||||||
@ -154,7 +169,11 @@ impl PostController for PostControllerImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_post(&self, post: CreatePostRequestDto) -> Result<PostResponseDto, PostError> {
|
async fn create_post(
|
||||||
|
&self,
|
||||||
|
post: CreatePostRequestDto,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<PostResponseDto, PostError> {
|
||||||
let label_ids = post.label_ids.clone();
|
let label_ids = post.label_ids.clone();
|
||||||
let post_entity = post.into_entity();
|
let post_entity = post.into_entity();
|
||||||
|
|
||||||
@ -163,13 +182,14 @@ impl PostController for PostControllerImpl {
|
|||||||
.execute(post_entity, &label_ids)
|
.execute(post_entity, &label_ids)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.get_post_by_id(id).await
|
self.get_post_by_id(id, Some(user_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_post(
|
async fn update_post(
|
||||||
&self,
|
&self,
|
||||||
id: i32,
|
id: i32,
|
||||||
post: UpdatePostRequestDto,
|
post: UpdatePostRequestDto,
|
||||||
|
user_id: i32,
|
||||||
) -> Result<PostResponseDto, PostError> {
|
) -> Result<PostResponseDto, PostError> {
|
||||||
let label_ids = post.label_ids.clone();
|
let label_ids = post.label_ids.clone();
|
||||||
let post_entity = post.into_entity(id);
|
let post_entity = post.into_entity(id);
|
||||||
@ -178,6 +198,6 @@ impl PostController for PostControllerImpl {
|
|||||||
.execute(post_entity, &label_ids)
|
.execute(post_entity, &label_ids)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.get_post_by_id(id).await
|
self.get_post_by_id(id, Some(user_id)).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ use std::fmt::Display;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PostError {
|
pub enum PostError {
|
||||||
NotFound,
|
NotFound,
|
||||||
|
Unauthorized,
|
||||||
Unexpected(anyhow::Error),
|
Unexpected(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ impl Display for PostError {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
PostError::NotFound => write!(f, "Post not found"),
|
PostError::NotFound => write!(f, "Post not found"),
|
||||||
|
PostError::Unauthorized => write!(f, "Unauthorized access to post"),
|
||||||
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,11 @@ use crate::{
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait GetAllPostInfoUseCase: Send + Sync {
|
pub trait GetAllPostInfoUseCase: Send + Sync {
|
||||||
async fn execute(&self, is_published_only: bool) -> Result<Vec<PostInfo>, PostError>;
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
is_published_only: bool,
|
||||||
|
user_id: Option<i32>,
|
||||||
|
) -> Result<Vec<PostInfo>, PostError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetAllPostInfoUseCaseImpl {
|
pub struct GetAllPostInfoUseCaseImpl {
|
||||||
@ -24,7 +28,15 @@ impl GetAllPostInfoUseCaseImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl GetAllPostInfoUseCase for GetAllPostInfoUseCaseImpl {
|
impl GetAllPostInfoUseCase for GetAllPostInfoUseCaseImpl {
|
||||||
async fn execute(&self, is_published_only: bool) -> Result<Vec<PostInfo>, PostError> {
|
async fn execute(
|
||||||
self.post_repository.get_all_post_info(is_published_only).await
|
&self,
|
||||||
|
is_published_only: bool,
|
||||||
|
user_id: Option<i32>,
|
||||||
|
) -> Result<Vec<PostInfo>, PostError> {
|
||||||
|
let is_published_only = is_published_only && user_id.is_some();
|
||||||
|
|
||||||
|
self.post_repository
|
||||||
|
.get_all_post_info(is_published_only)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use crate::{
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait GetFullPostUseCase: Send + Sync {
|
pub trait GetFullPostUseCase: Send + Sync {
|
||||||
async fn execute(&self, id: i32) -> Result<Post, PostError>;
|
async fn execute(&self, id: i32, user_id: Option<i32>) -> Result<Post, PostError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetFullPostUseCaseImpl {
|
pub struct GetFullPostUseCaseImpl {
|
||||||
@ -24,7 +24,13 @@ impl GetFullPostUseCaseImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl GetFullPostUseCase for GetFullPostUseCaseImpl {
|
impl GetFullPostUseCase for GetFullPostUseCaseImpl {
|
||||||
async fn execute(&self, id: i32) -> Result<Post, PostError> {
|
async fn execute(&self, id: i32, user_id: Option<i32>) -> Result<Post, PostError> {
|
||||||
self.post_repository.get_post_by_id(id).await
|
let post = self.post_repository.get_post_by_id(id).await?;
|
||||||
|
|
||||||
|
if post.info.published_time.is_none() && user_id.is_none() {
|
||||||
|
return Err(PostError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,11 @@ use crate::{
|
|||||||
pub async fn create_post_handler(
|
pub async fn create_post_handler(
|
||||||
post_controller: web::Data<dyn PostController>,
|
post_controller: web::Data<dyn PostController>,
|
||||||
post_dto: web::Json<CreatePostRequestDto>,
|
post_dto: web::Json<CreatePostRequestDto>,
|
||||||
_: UserId,
|
user_id: UserId,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let result = post_controller.create_post(post_dto.into_inner()).await;
|
let result = post_controller
|
||||||
|
.create_post(post_dto.into_inner(), user_id.get())
|
||||||
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(post) => HttpResponse::Created().json(post),
|
Ok(post) => HttpResponse::Created().json(post),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use auth::framework::web::auth_middleware::UserId;
|
||||||
use sentry::integrations::anyhow::capture_anyhow;
|
use sentry::integrations::anyhow::capture_anyhow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -15,6 +16,7 @@ use crate::{
|
|||||||
path = "/post",
|
path = "/post",
|
||||||
tag = "post",
|
tag = "post",
|
||||||
summary = "Get all post information",
|
summary = "Get all post information",
|
||||||
|
description = "`is_published_only` query is only available for authenticated users.",
|
||||||
params(
|
params(
|
||||||
PostQueryDto
|
PostQueryDto
|
||||||
),
|
),
|
||||||
@ -25,8 +27,11 @@ use crate::{
|
|||||||
pub async fn get_all_post_info_handler(
|
pub async fn get_all_post_info_handler(
|
||||||
post_controller: web::Data<dyn PostController>,
|
post_controller: web::Data<dyn PostController>,
|
||||||
query: web::Query<PostQueryDto>,
|
query: web::Query<PostQueryDto>,
|
||||||
|
user_id: Option<UserId>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let result = post_controller.get_all_post_info(query.into_inner()).await;
|
let result = post_controller
|
||||||
|
.get_all_post_info(query.into_inner(), user_id.map(|id| id.get()))
|
||||||
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
|
use auth::framework::web::auth_middleware::UserId;
|
||||||
use sentry::integrations::anyhow::capture_anyhow;
|
use sentry::integrations::anyhow::capture_anyhow;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -11,6 +12,7 @@ use crate::{
|
|||||||
path = "/post/{id}",
|
path = "/post/{id}",
|
||||||
tag = "post",
|
tag = "post",
|
||||||
summary = "Get post by ID",
|
summary = "Get post by ID",
|
||||||
|
description = "Only authenticated users can access unpublished posts.",
|
||||||
responses (
|
responses (
|
||||||
(status = 200, body = PostResponseDto),
|
(status = 200, body = PostResponseDto),
|
||||||
(status = 404, description = "Post not found")
|
(status = 404, description = "Post not found")
|
||||||
@ -19,14 +21,18 @@ use crate::{
|
|||||||
pub async fn get_post_by_id_handler(
|
pub async fn get_post_by_id_handler(
|
||||||
post_controller: web::Data<dyn PostController>,
|
post_controller: web::Data<dyn PostController>,
|
||||||
path: web::Path<i32>,
|
path: web::Path<i32>,
|
||||||
|
user_id: Option<UserId>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
let result = post_controller.get_post_by_id(id).await;
|
let result = post_controller
|
||||||
|
.get_post_by_id(id, user_id.map(|id| id.get()))
|
||||||
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(post) => HttpResponse::Ok().json(post),
|
Ok(post) => HttpResponse::Ok().json(post),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||||
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::Unexpected(e) => {
|
PostError::Unexpected(e) => {
|
||||||
capture_anyhow(&e);
|
capture_anyhow(&e);
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
|
@ -38,6 +38,7 @@ pub async fn update_label_handler(
|
|||||||
Ok(label) => HttpResponse::Ok().json(label),
|
Ok(label) => HttpResponse::Ok().json(label),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||||
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::Unexpected(e) => {
|
PostError::Unexpected(e) => {
|
||||||
capture_anyhow(&e);
|
capture_anyhow(&e);
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
|
@ -27,15 +27,18 @@ pub async fn update_post_handler(
|
|||||||
post_controller: web::Data<dyn PostController>,
|
post_controller: web::Data<dyn PostController>,
|
||||||
path: web::Path<i32>,
|
path: web::Path<i32>,
|
||||||
post_dto: web::Json<UpdatePostRequestDto>,
|
post_dto: web::Json<UpdatePostRequestDto>,
|
||||||
_: UserId,
|
user_id: UserId,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
let result = post_controller.update_post(id, post_dto.into_inner()).await;
|
let result = post_controller
|
||||||
|
.update_post(id, post_dto.into_inner(), user_id.get())
|
||||||
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(post) => HttpResponse::Ok().json(post),
|
Ok(post) => HttpResponse::Ok().json(post),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||||
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::Unexpected(e) => {
|
PostError::Unexpected(e) => {
|
||||||
capture_anyhow(&e);
|
capture_anyhow(&e);
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user