feat: add validation for semantic ID in post and update error handling
All checks were successful
Frontend CI / build (push) Successful in 1m23s

This commit is contained in:
SquidSpirit 2025-10-12 16:36:48 +08:00
parent 43582af2a5
commit 8f91d815bc
15 changed files with 96 additions and 36 deletions

9
backend/Cargo.lock generated
View File

@ -2407,6 +2407,7 @@ dependencies = [
"auth",
"chrono",
"common",
"regex",
"sentry",
"serde",
"sqlx",
@ -2638,9 +2639,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433"
dependencies = [
"aho-corasick",
"memchr",
@ -2650,9 +2651,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
dependencies = [
"aho-corasick",
"memchr",

View File

@ -5,7 +5,6 @@ members = [
"feature/common",
"feature/image",
"feature/post",
"feature/common",
]
resolver = "2"
@ -33,6 +32,7 @@ openidconnect = { version = "4.0.1", features = [
"reqwest-blocking",
] }
percent-encoding = "2.3.1"
regex = "1.12.1"
sentry = { version = "0.42.0", features = ["actix", "anyhow"] }
serde = { version = "1.0.219", features = ["derive"] }
sqlx = { version = "0.8.5", features = [

View File

@ -8,6 +8,7 @@ actix-web.workspace = true
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
regex.workspace = true
sentry.workspace = true
serde.workspace = true
sqlx.workspace = true

View File

@ -4,6 +4,7 @@ use std::fmt::Display;
pub enum PostError {
NotFound,
Unauthorized,
InvalidSemanticId,
Unexpected(anyhow::Error),
}
@ -12,6 +13,10 @@ impl Display for PostError {
match self {
PostError::NotFound => write!(f, "Post not found"),
PostError::Unauthorized => write!(f, "Unauthorized access to post"),
PostError::InvalidSemanticId => write!(
f,
"Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_\\-]+$`"
),
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}

View File

@ -25,8 +25,7 @@ impl CreatePostUseCaseImpl {
#[async_trait]
impl CreatePostUseCase for CreatePostUseCaseImpl {
async fn execute(&self, post: Post, label_ids: &[i32]) -> Result<i32, PostError> {
self.post_repository
.create_post(post, label_ids)
.await
post.validate()?;
self.post_repository.create_post(post, label_ids).await
}
}

View File

@ -25,6 +25,7 @@ impl UpdatePostUseCaseImpl {
#[async_trait]
impl UpdatePostUseCase for UpdatePostUseCaseImpl {
async fn execute(&self, post: Post, label_ids: &[i32]) -> Result<(), PostError> {
post.validate()?;
self.post_repository.update_post(post, label_ids).await
}
}

View File

@ -1,3 +1,5 @@
use crate::application::error::post_error::PostError;
use super::post_info::PostInfo;
pub struct Post {
@ -5,3 +7,10 @@ pub struct Post {
pub info: PostInfo,
pub content: String,
}
impl Post {
pub fn validate(&self) -> Result<(), PostError> {
self.info.validate()?;
Ok(())
}
}

View File

@ -1,4 +1,7 @@
use chrono::{DateTime, Utc};
use regex::Regex;
use crate::application::error::post_error::PostError;
use super::label::Label;
@ -11,3 +14,18 @@ pub struct PostInfo {
pub labels: Vec<Label>,
pub published_time: Option<DateTime<Utc>>,
}
impl PostInfo {
pub fn validate(&self) -> Result<(), PostError> {
if self.semantic_id.parse::<i32>().is_ok() {
return Err(PostError::InvalidSemanticId);
}
let re = Regex::new(r"^[0-9a-zA-Z_\-]+$").unwrap();
if !re.is_match(&self.semantic_id) {
return Err(PostError::InvalidSemanticId);
}
Ok(())
}
}

View File

@ -32,12 +32,16 @@ pub async fn create_label_handler(
match result {
Ok(label) => HttpResponse::Created().json(label),
Err(e) => {
match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
Err(e) => match e {
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::NotFound | PostError::InvalidSemanticId => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()
}
},
}
}

View File

@ -34,12 +34,17 @@ pub async fn create_post_handler(
match result {
Ok(post) => HttpResponse::Created().json(post),
Err(e) => {
match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
Err(e) => match e {
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
PostError::NotFound => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()
}
},
}
}

View File

@ -23,12 +23,15 @@ pub async fn get_all_labels_handler(
match result {
Ok(labels) => HttpResponse::Ok().json(labels),
Err(e) => {
match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
Err(e) => match e {
PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()
}
},
}
}

View File

@ -35,12 +35,15 @@ pub async fn get_all_post_info_handler(
match result {
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
Err(e) => {
match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
Err(e) => match e {
PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()
}
},
}
}

View File

@ -1,4 +1,5 @@
use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow;
@ -33,6 +34,10 @@ pub async fn get_post_by_id_handler(
Err(e) => match e {
PostError::NotFound => HttpResponse::NotFound().finish(),
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::InvalidSemanticId => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()

View File

@ -1,4 +1,5 @@
use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow;
@ -39,6 +40,10 @@ pub async fn update_label_handler(
Err(e) => match e {
PostError::NotFound => HttpResponse::NotFound().finish(),
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::InvalidSemanticId => {
capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish()
}
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()

View File

@ -39,6 +39,7 @@ pub async fn update_post_handler(
Err(e) => match e {
PostError::NotFound => HttpResponse::NotFound().finish(),
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
PostError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()