From d083f8ae3d9c789c4861ae2cce71435331bf4f99 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 4 May 2026 21:41:07 +0200 Subject: [PATCH] refactor: use constant for minimum password length and API rate limit Co-authored-by: Copilot --- crates/application/src/use_cases/register.rs | 8 +- crates/presentation/src/routes.rs | 89 ++++++++++++-------- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/crates/application/src/use_cases/register.rs b/crates/application/src/use_cases/register.rs index c3a173b..3cd7f05 100644 --- a/crates/application/src/use_cases/register.rs +++ b/crates/application/src/use_cases/register.rs @@ -2,12 +2,14 @@ use domain::{errors::DomainError, models::User, value_objects::Email}; use crate::{commands::RegisterCommand, context::AppContext}; +const MIN_PASSWORD_LENGTH: usize = 8; + pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> { if !ctx.config.allow_registration { return Err(DomainError::Unauthorized("Registration is disabled".into())); } - if cmd.password.len() < 8 { + if cmd.password.len() < MIN_PASSWORD_LENGTH { return Err(DomainError::ValidationError( "Password must be at least 8 characters".into(), )); @@ -16,7 +18,9 @@ pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), Domai let email = Email::new(cmd.email)?; if ctx.user_repository.find_by_email(&email).await?.is_some() { - return Err(DomainError::ValidationError("Email already registered".into())); + return Err(DomainError::ValidationError( + "Email already registered".into(), + )); } let hash = ctx.password_hasher.hash(&cmd.password).await?; diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 88eb7a1..a574544 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -9,6 +9,8 @@ use tower_http::{services::ServeDir, trace::TraceLayer}; use crate::{handlers, state::AppState}; +const API_RATE_LIMIT: u64 = 20; // 20 requests per minute globally for API routes + /// Simple global rate limiter: tracks request count per 60-second window. /// Not per-IP — suitable for a low-traffic personal app. #[derive(Clone)] @@ -55,20 +57,62 @@ pub fn build_router(state: AppState) -> Router { fn html_routes() -> Router { // Auth routes: 20 requests per minute globally. - let limiter = RateLimiter::new(20); + let limiter = RateLimiter::new(API_RATE_LIMIT); let auth = Router::new() .route( "/login", - routing::get(handlers::html::get_login_page) - .post(handlers::html::post_login), + routing::get(handlers::html::get_login_page).post(handlers::html::post_login), ) .route("/logout", routing::get(handlers::html::get_logout)) .route( "/register", - routing::get(handlers::html::get_register_page) - .post(handlers::html::post_register), + routing::get(handlers::html::get_register_page).post(handlers::html::post_register), ) - .route_layer(middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| { + .route_layer(middleware::from_fn( + move |req: axum::extract::Request, next: middleware::Next| { + let limiter = limiter.clone(); + async move { + if limiter.check() { + next.run(req).await + } else { + StatusCode::TOO_MANY_REQUESTS.into_response() + } + } + }, + )); + + Router::new() + .route("/", routing::get(handlers::html::get_activity_feed)) + .route("/users", routing::get(handlers::html::get_users_list)) + .route( + "/users/{id}", + routing::get(handlers::html::get_user_profile), + ) + .merge(auth) + .route( + "/reviews/new", + routing::get(handlers::html::get_new_review_page), + ) + .route("/reviews", routing::post(handlers::html::post_review)) + .route( + "/reviews/{id}/delete", + routing::post(handlers::html::post_delete_review), + ) + .route( + "/posters/{path}", + routing::get(handlers::posters::get_poster), + ) + .route("/feed.rss", routing::get(handlers::rss::get_feed)) + .route( + "/users/{id}/feed.rss", + routing::get(handlers::rss::get_user_feed), + ) +} + +fn api_routes() -> Router { + let limiter = RateLimiter::new(API_RATE_LIMIT); + let auth_rate_limit = + middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| { let limiter = limiter.clone(); async move { if limiter.check() { @@ -77,33 +121,7 @@ fn html_routes() -> Router { StatusCode::TOO_MANY_REQUESTS.into_response() } } - })); - - Router::new() - .route("/", routing::get(handlers::html::get_activity_feed)) - .route("/users", routing::get(handlers::html::get_users_list)) - .route("/users/{id}", routing::get(handlers::html::get_user_profile)) - .merge(auth) - .route("/reviews/new", routing::get(handlers::html::get_new_review_page)) - .route("/reviews", routing::post(handlers::html::post_review)) - .route("/reviews/{id}/delete", routing::post(handlers::html::post_delete_review)) - .route("/posters/{path}", routing::get(handlers::posters::get_poster)) - .route("/feed.rss", routing::get(handlers::rss::get_feed)) - .route("/users/{id}/feed.rss", routing::get(handlers::rss::get_user_feed)) -} - -fn api_routes() -> Router { - let limiter = RateLimiter::new(20); - let auth_rate_limit = middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| { - let limiter = limiter.clone(); - async move { - if limiter.check() { - next.run(req).await - } else { - StatusCode::TOO_MANY_REQUESTS.into_response() - } - } - }); + }); Router::new().nest( "/api", @@ -114,7 +132,10 @@ fn api_routes() -> Router { routing::get(handlers::api::get_review_history), ) .route("/reviews", routing::post(handlers::api::post_review)) - .route("/reviews/{id}", routing::delete(handlers::api::delete_review)) + .route( + "/reviews/{id}", + routing::delete(handlers::api::delete_review), + ) .route( "/movies/{id}/sync-poster", routing::post(handlers::api::sync_poster),