feat: post and comment length limit

fix: 🚑 user extraction from url
This commit is contained in:
Ahmet Kaan GÜMÜŞ 2025-01-29 17:01:56 +03:00
parent 6d3b6a4e79
commit 11f101638e
13 changed files with 185 additions and 104 deletions

View file

@ -18,7 +18,7 @@ strip = "symbols"
tokio = { version = "1.43.0", default-features = false,features = ["macros", "rt-multi-thread", "time"] }
serde = { version = "1.0.217", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.135" , default-features = false}
axum = { version = "0.8.1", default-features = false, features = ["http1", "json", "tokio"]}
axum = { version = "0.8.1", default-features = false, features = ["http1", "json", "original-uri", "tokio"]}
chrono = { version = "0.4.39", default-features = false, features = ["serde"] }
jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] }
lettre = { version = "0.11.11", default-features = false, features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"] }

View file

@ -4,3 +4,5 @@ otp_time_limit = 15
login_token_expiration_time_limit = 15
login_token_refresh_time_limit = 30
concurrency_limit = -1
post_length_limit = 1048576
comment_length_limit = 1048576

View file

@ -6,3 +6,8 @@ CREATE TABLE IF NOT EXISTS "user_contact"(
PRIMARY KEY (user_id, contact_id),
UNIQUE (contact_id, contact_value)
);
INSERT INTO "user_contact"(user_id, contact_id, contact_value)
VALUES (0, 0, 'builder@rust_forum.com')
ON CONFLICT(user_id, contact_id) DO UPDATE SET
"contact_value" = 'builder@rust_forum.com';

View file

@ -5,6 +5,7 @@ pub enum ForumInputError {
ForbiddenCharacter,
ForbiddenString,
EmptyParameter,
TooLong,
}
impl std::fmt::Display for ForumInputError {
@ -17,6 +18,7 @@ impl std::fmt::Display for ForumInputError {
write!(f, "Forbidden String Detected")
}
&ForumInputError::EmptyParameter => write!(f, "Parameter is Empty"),
ForumInputError::TooLong => write!(f, "Input is Too Long"),
}
}
}

View file

@ -4,6 +4,8 @@ use crate::database::user_contact;
use super::user::User;
const CONTACT_DEFAULT_ID_FOR_EMAIL: i64 = 0;
#[derive(Debug, Serialize, Deserialize)]
pub struct UserContact {
pub user_id: i64,
@ -20,10 +22,21 @@ impl UserContact {
user_contact::create(&user.user_id, contact_id, contact_value).await
}
pub async fn create_for_email(
user: &User,
contact_value: &String,
) -> Result<UserContact, sqlx::Error> {
user_contact::create(&user.user_id, &CONTACT_DEFAULT_ID_FOR_EMAIL, contact_value).await
}
pub async fn read(user: &User, contact_id: &i64) -> Result<UserContact, sqlx::Error> {
user_contact::read(&user.user_id, contact_id).await
}
pub async fn read_for_email(user: &User) -> Result<UserContact, sqlx::Error> {
user_contact::read(&user.user_id, &CONTACT_DEFAULT_ID_FOR_EMAIL).await
}
pub async fn read_for_value(
contact_id: &i64,
contact_value: &String,
@ -31,6 +44,10 @@ impl UserContact {
user_contact::read_for_value(contact_id, contact_value).await
}
pub async fn read_for_email_value(contact_value: &String) -> Result<UserContact, sqlx::Error> {
user_contact::read_for_value(&CONTACT_DEFAULT_ID_FOR_EMAIL, contact_value).await
}
pub async fn update(
user: &User,
contact_id: &i64,

View file

@ -8,7 +8,6 @@ pub mod utils;
use std::sync::LazyLock;
use sqlx::{Pool, Postgres};
use utils::naive_toml_parser;
pub static SERVER_CONFIG: LazyLock<ServerConfig> = LazyLock::new(ServerConfig::default);
@ -51,6 +50,8 @@ pub struct ServerConfig {
pub login_token_expiration_time_limit: usize,
pub login_token_refresh_time_limit: usize,
pub concurrency_limit: usize,
pub post_length_limit: usize,
pub comment_length_limit: usize,
}
impl Default for ServerConfig {
@ -72,14 +73,11 @@ impl Default for ServerConfig {
),
login_token_refresh_time_limit: value_or_max(server_configs.pop_front().unwrap()),
concurrency_limit: value_or_semaphore_max(server_configs.pop_front().unwrap()),
post_length_limit: server_configs.pop_front().unwrap().parse().unwrap(),
comment_length_limit: server_configs.pop_front().unwrap().parse().unwrap(),
}
} else {
panic!("Server Config File Must Include [server_config] at the First Line")
}
}
}
#[derive(Debug, Clone)]
pub struct AppState {
pub database_connection: Pool<Postgres>,
}

View file

@ -12,7 +12,6 @@ pub mod user;
pub mod user_contact;
use axum::{http::StatusCode, response::IntoResponse, routing::get, Router};
use middleware::by_authorization_token;
use tower::limit::ConcurrencyLimitLayer;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
@ -32,8 +31,6 @@ pub async fn route(concurrency_limit: &usize) -> Router {
.nest("/contacts", contact::route())
.nest("/user_contacts", user_contact::route())
.nest("/admin", admin::route())
// todo just for beta I think
.route_layer(axum::middleware::from_fn(by_authorization_token))
.layer(CorsLayer::permissive())
.layer(ConcurrencyLimitLayer::new(*concurrency_limit))
.layer(TraceLayer::new_for_http())

View file

@ -7,12 +7,20 @@ pub mod role;
pub mod user;
pub mod user_contact;
use axum::Router;
use axum::{response::IntoResponse, routing::get, Router};
use super::middleware::builder_or_admin_by_authorization_token;
use super::middleware::{builder_or_admin_by_authorization_token, by_uri_then_insert};
async fn a() -> impl IntoResponse {
"HEY"
}
pub fn route() -> Router {
Router::new()
.route(
"/users/{user_id}",
get(a).route_layer(axum::middleware::from_fn(by_uri_then_insert)),
)
.nest("/logins", login::route())
.nest("/users", user::route())
.nest("/roles", role::route())

View file

@ -10,7 +10,10 @@ use axum::{
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::{feature::user::User, routing::middleware::by_uri_then_insert};
use crate::{
feature::{user::User, user_contact::UserContact},
routing::middleware::by_uri_then_insert,
};
#[derive(Debug, Serialize, Deserialize)]
struct CreateUser {
@ -18,6 +21,7 @@ struct CreateUser {
surname: String,
gender: bool,
birth_date: NaiveDate,
email: String,
}
#[derive(Debug, Serialize, Deserialize)]
@ -78,7 +82,13 @@ async fn create(Json(create_user): Json<CreateUser>) -> impl IntoResponse {
)
.await
{
Ok(user) => (StatusCode::CREATED, Json(serde_json::json!(user))),
Ok(user) => match UserContact::create_for_email(&user, &create_user.email).await {
Ok(_) => (StatusCode::CREATED, Json(serde_json::json!(user))),
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),
),
},
Err(err_val) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(err_val.to_string())),

View file

@ -9,7 +9,11 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use crate::feature::{comment::Comment, user::User};
use crate::{
error::ForumInputError,
feature::{comment::Comment, user::User},
SERVER_CONFIG,
};
use super::middleware::by_authorization_token_then_insert;
@ -52,6 +56,12 @@ async fn create(
Extension(user): Extension<Arc<User>>,
Json(create_comment): Json<CreateComment>,
) -> impl IntoResponse {
if create_comment.comment.len() > SERVER_CONFIG.comment_length_limit {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(ForumInputError::TooLong)),
);
} else {
match Comment::create(
&user.user_id,
&create_comment.post_id,
@ -66,6 +76,7 @@ async fn create(
),
}
}
}
async fn read(Path(comment_id): Path<i64>) -> impl IntoResponse {
match Comment::read(&comment_id).await {
@ -82,6 +93,12 @@ async fn update(
Path(comment_id): Path<i64>,
Json(update_comment): Json<UpdateComment>,
) -> impl IntoResponse {
if update_comment.comment.len() > SERVER_CONFIG.comment_length_limit {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(ForumInputError::TooLong)),
);
} else {
match Comment::update(&comment_id, &user.user_id, &update_comment.comment).await {
Ok(comment) => (StatusCode::ACCEPTED, Json(serde_json::json!(comment))),
Err(err_val) => (
@ -90,6 +107,7 @@ async fn update(
),
}
}
}
async fn delete_(
Extension(user): Extension<Arc<User>>,

View file

@ -14,8 +14,6 @@ use super::middleware::{
by_authorization_token_then_insert, user_and_token_then_insert, UserAndAuthorizationToken,
};
const CONTACT_EMAIL_DEFAULT_ID: i64 = 0;
#[derive(Debug, Serialize, Deserialize)]
struct CreateOneTimePassword {
pub user_email: String,
@ -49,17 +47,17 @@ pub fn route() -> Router {
by_authorization_token_then_insert,
)),
)
.route("/count/users", get(count_all_for_user))
.route(
"/count/users",
get(count_all_for_user).route_layer(axum::middleware::from_fn(
by_authorization_token_then_insert,
)),
)
}
async fn create_one_time_password(
Json(create_one_time_password): Json<CreateOneTimePassword>,
) -> impl IntoResponse {
match UserContact::read_for_value(
&CONTACT_EMAIL_DEFAULT_ID,
&create_one_time_password.user_email,
)
.await
{
match UserContact::read_for_email_value(&create_one_time_password.user_email).await {
Ok(user_contact) => match User::read(&user_contact.user_id).await {
Ok(user) => {
match OneTimePassword::new(&user, &create_one_time_password.user_email).await {
@ -83,7 +81,7 @@ async fn create_one_time_password(
}
}
async fn create(Json(create_login): Json<CreateLogin>) -> impl IntoResponse {
match UserContact::read_for_value(&CONTACT_EMAIL_DEFAULT_ID, &create_login.user_email).await {
match UserContact::read_for_email_value(&create_login.user_email).await {
Ok(user_contact) => match User::read(&user_contact.user_id).await {
Ok(user) => {
let one_time_password =

View file

@ -1,7 +1,7 @@
use std::sync::Arc;
use axum::{
extract::Request,
extract::{OriginalUri, Request},
http::{self, HeaderMap, StatusCode, Uri},
middleware::Next,
response::IntoResponse,
@ -64,11 +64,11 @@ async fn user_extraction_from_uri(request_uri: &Uri) -> Result<User, ForumAuthEr
let request_uri_parts = request_uri.path().split('/').collect::<Vec<&str>>();
for (index, uri_part) in request_uri_parts.iter().enumerate() {
if *uri_part == "users" {
if let Some(user_id) = request_uri_parts.get(index) {
if let Ok(user_id) = (*user_id).parse::<i64>() {
User::read(&user_id).await.map_err(|err_val| {
if let Some(user_id) = request_uri_parts.get(index + 1) {
if let Ok(user_id) = user_id.parse::<i64>() {
return User::read(&user_id).await.map_err(|err_val| {
ForumAuthError::AuthenticationFailed(err_val.to_string())
})?;
});
}
}
}
@ -86,10 +86,7 @@ async fn user_from_header_and_target_user_from_uri_extraction(
Ok(UserAndTargetUser { user, target_user })
}
pub async fn user_and_token_then_insert(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
pub async fn user_and_token_then_insert(mut request: Request, next: Next) -> impl IntoResponse {
if let Ok(authorization_token) = authorization_token_extraction(&request.headers()).await {
if let Ok(user) = user_extraction_from_authorization_token(&authorization_token).await {
let user_and_token = Arc::new(UserAndAuthorizationToken {
@ -98,110 +95,115 @@ pub async fn user_and_token_then_insert(
});
request.extensions_mut().insert(user_and_token);
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}
pub async fn by_authorization_token(
request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
pub async fn by_authorization_token(request: Request, next: Next) -> impl IntoResponse {
match user_extraction_from_header(request.headers()).await {
Ok(_) => Ok(next.run(request).await),
Err(_) => Err(StatusCode::FORBIDDEN),
Ok(_) => next.run(request).await,
Err(_) => StatusCode::FORBIDDEN.into_response(),
}
}
pub async fn by_authorization_token_then_insert(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
) -> impl IntoResponse {
match user_extraction_from_header(request.headers()).await {
Ok(user) => {
let user = Arc::new(user);
request.extensions_mut().insert(user);
Ok(next.run(request).await)
next.run(request).await
}
Err(_) => Err(StatusCode::FORBIDDEN),
Err(_) => StatusCode::FORBIDDEN.into_response(),
}
}
pub async fn by_uri_then_insert(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
if let Ok(target_user) = user_extraction_from_uri(request.uri()).await {
pub async fn by_uri_then_insert(mut request: Request, next: Next) -> impl IntoResponse {
if let Ok(target_user) = user_extraction_from_uri(
request
.extensions()
.get::<OriginalUri>()
.expect("Shouldn't panic, how we couldn't have uri"),
)
.await
{
let target_user = Arc::new(target_user);
request.extensions_mut().insert(target_user);
return Ok(next.run(request).await);
return next.run(request).await;
}
Err(StatusCode::BAD_REQUEST)
StatusCode::BAD_REQUEST.into_response()
}
pub async fn builder_by_authorization_token(
request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
pub async fn builder_by_authorization_token(request: Request, next: Next) -> impl IntoResponse {
if let Ok(user) = user_extraction_from_header(request.headers()).await {
if User::is_builder(&user).await {
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}
pub async fn builder_by_authorization_token_then_insert(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
) -> impl IntoResponse {
if let Ok(user) = user_extraction_from_header(request.headers()).await {
if User::is_builder(&user).await {
let user = Arc::new(user);
request.extensions_mut().insert(user);
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}
pub async fn builder_or_admin_by_authorization_token(
request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
) -> impl IntoResponse {
if let Ok(user) = user_extraction_from_header(request.headers()).await {
if User::is_builder_or_admin(&user).await {
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}
pub async fn builder_or_admin_by_authorization_token_then_insert(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
) -> impl IntoResponse {
if let Ok(user) = user_extraction_from_header(request.headers()).await {
if User::is_builder_or_admin(&user).await {
let user = Arc::new(user);
request.extensions_mut().insert(user);
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}
pub async fn builder_by_authorization_token_and_target_user_by_uri_then_insert_target(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
if let Ok(user_and_target_user) =
user_from_header_and_target_user_from_uri_extraction(request.headers(), request.uri()).await
) -> impl IntoResponse {
if let Ok(user_and_target_user) = user_from_header_and_target_user_from_uri_extraction(
request.headers(),
request
.extensions()
.get::<OriginalUri>()
.expect("Shouldn't panic, how we couldn't have uri"),
)
.await
{
let user = user_and_target_user.user;
let target_user = user_and_target_user.target_user;
@ -210,18 +212,24 @@ pub async fn builder_by_authorization_token_and_target_user_by_uri_then_insert_t
let target_user = Arc::new(target_user);
request.extensions_mut().insert(target_user);
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}
pub async fn builder_or_admin_by_authorization_token_and_target_user_by_uri_then_insert_target(
mut request: Request,
next: Next,
) -> Result<impl IntoResponse, StatusCode> {
if let Ok(user_and_target_user) =
user_from_header_and_target_user_from_uri_extraction(request.headers(), request.uri()).await
) -> impl IntoResponse {
if let Ok(user_and_target_user) = user_from_header_and_target_user_from_uri_extraction(
request.headers(),
request
.extensions()
.get::<OriginalUri>()
.expect("Shouldn't panic, how we couldn't have uri"),
)
.await
{
let user = user_and_target_user.user;
let target_user = user_and_target_user.target_user;
@ -230,8 +238,8 @@ pub async fn builder_or_admin_by_authorization_token_and_target_user_by_uri_then
let target_user = Arc::new(target_user);
request.extensions_mut().insert(target_user);
return Ok(next.run(request).await);
return next.run(request).await;
}
}
Err(StatusCode::FORBIDDEN)
StatusCode::FORBIDDEN.into_response()
}

View file

@ -9,7 +9,11 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use crate::feature::{post::Post, user::User};
use crate::{
error::ForumInputError,
feature::{post::Post, user::User},
SERVER_CONFIG,
};
use super::middleware::{by_authorization_token_then_insert, by_uri_then_insert};
@ -53,6 +57,12 @@ async fn create(
Extension(user): Extension<Arc<User>>,
Json(create_post): Json<CreatePost>,
) -> impl IntoResponse {
if create_post.post.len() > SERVER_CONFIG.post_length_limit {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(ForumInputError::TooLong)),
);
} else {
match Post::create(&user.user_id, &create_post.post).await {
Ok(post) => (StatusCode::CREATED, Json(serde_json::json!(post))),
Err(err_val) => (
@ -61,6 +71,7 @@ async fn create(
),
}
}
}
async fn read(Path(post_id): Path<i64>) -> impl IntoResponse {
match Post::read(&post_id).await {
@ -77,6 +88,12 @@ async fn update(
Path(post_id): Path<i64>,
Json(update_post): Json<UpdatePost>,
) -> impl IntoResponse {
if update_post.post.len() > SERVER_CONFIG.post_length_limit {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!(ForumInputError::TooLong)),
);
} else {
match Post::update(&post_id, &user.user_id, &update_post.post).await {
Ok(post) => (StatusCode::ACCEPTED, Json(serde_json::json!(post))),
Err(err_val) => (
@ -85,6 +102,7 @@ async fn update(
),
}
}
}
async fn delete_(
Extension(user): Extension<Arc<User>>,