refactor: use constant for minimum password length and API rate limit
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -2,12 +2,14 @@ use domain::{errors::DomainError, models::User, value_objects::Email};
|
|||||||
|
|
||||||
use crate::{commands::RegisterCommand, context::AppContext};
|
use crate::{commands::RegisterCommand, context::AppContext};
|
||||||
|
|
||||||
|
const MIN_PASSWORD_LENGTH: usize = 8;
|
||||||
|
|
||||||
pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> {
|
pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), DomainError> {
|
||||||
if !ctx.config.allow_registration {
|
if !ctx.config.allow_registration {
|
||||||
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.password.len() < 8 {
|
if cmd.password.len() < MIN_PASSWORD_LENGTH {
|
||||||
return Err(DomainError::ValidationError(
|
return Err(DomainError::ValidationError(
|
||||||
"Password must be at least 8 characters".into(),
|
"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)?;
|
let email = Email::new(cmd.email)?;
|
||||||
|
|
||||||
if ctx.user_repository.find_by_email(&email).await?.is_some() {
|
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?;
|
let hash = ctx.password_hasher.hash(&cmd.password).await?;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use tower_http::{services::ServeDir, trace::TraceLayer};
|
|||||||
|
|
||||||
use crate::{handlers, state::AppState};
|
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.
|
/// Simple global rate limiter: tracks request count per 60-second window.
|
||||||
/// Not per-IP — suitable for a low-traffic personal app.
|
/// Not per-IP — suitable for a low-traffic personal app.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -55,20 +57,62 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
|
|
||||||
fn html_routes() -> Router<AppState> {
|
fn html_routes() -> Router<AppState> {
|
||||||
// Auth routes: 20 requests per minute globally.
|
// Auth routes: 20 requests per minute globally.
|
||||||
let limiter = RateLimiter::new(20);
|
let limiter = RateLimiter::new(API_RATE_LIMIT);
|
||||||
let auth = Router::new()
|
let auth = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/login",
|
"/login",
|
||||||
routing::get(handlers::html::get_login_page)
|
routing::get(handlers::html::get_login_page).post(handlers::html::post_login),
|
||||||
.post(handlers::html::post_login),
|
|
||||||
)
|
)
|
||||||
.route("/logout", routing::get(handlers::html::get_logout))
|
.route("/logout", routing::get(handlers::html::get_logout))
|
||||||
.route(
|
.route(
|
||||||
"/register",
|
"/register",
|
||||||
routing::get(handlers::html::get_register_page)
|
routing::get(handlers::html::get_register_page).post(handlers::html::post_register),
|
||||||
.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<AppState> {
|
||||||
|
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();
|
let limiter = limiter.clone();
|
||||||
async move {
|
async move {
|
||||||
if limiter.check() {
|
if limiter.check() {
|
||||||
@@ -77,33 +121,7 @@ fn html_routes() -> Router<AppState> {
|
|||||||
StatusCode::TOO_MANY_REQUESTS.into_response()
|
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<AppState> {
|
|
||||||
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(
|
Router::new().nest(
|
||||||
"/api",
|
"/api",
|
||||||
@@ -114,7 +132,10 @@ fn api_routes() -> Router<AppState> {
|
|||||||
routing::get(handlers::api::get_review_history),
|
routing::get(handlers::api::get_review_history),
|
||||||
)
|
)
|
||||||
.route("/reviews", routing::post(handlers::api::post_review))
|
.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(
|
.route(
|
||||||
"/movies/{id}/sync-poster",
|
"/movies/{id}/sync-poster",
|
||||||
routing::post(handlers::api::sync_poster),
|
routing::post(handlers::api::sync_poster),
|
||||||
|
|||||||
Reference in New Issue
Block a user