feat(config): add rate limit configuration to AppConfig and update related usages
This commit is contained in:
@@ -18,6 +18,7 @@ COPY crates/adapters/template-askama/Cargo.toml crates/adapters/template-askam
|
|||||||
COPY crates/application/Cargo.toml crates/application/Cargo.toml
|
COPY crates/application/Cargo.toml crates/application/Cargo.toml
|
||||||
COPY crates/domain/Cargo.toml crates/domain/Cargo.toml
|
COPY crates/domain/Cargo.toml crates/domain/Cargo.toml
|
||||||
COPY crates/presentation/Cargo.toml crates/presentation/Cargo.toml
|
COPY crates/presentation/Cargo.toml crates/presentation/Cargo.toml
|
||||||
|
COPY crates/tui/Cargo.toml crates/tui/Cargo.toml
|
||||||
|
|
||||||
# Stub every crate so cargo can resolve and fetch deps
|
# Stub every crate so cargo can resolve and fetch deps
|
||||||
RUN find crates -name "Cargo.toml" | sed 's|/Cargo.toml||' | \
|
RUN find crates -name "Cargo.toml" | sed 's|/Cargo.toml||' | \
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub allow_registration: bool,
|
pub allow_registration: bool,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
|
pub rate_limit: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
@@ -11,6 +12,10 @@ impl AppConfig {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let base_url = std::env::var("BASE_URL")
|
let base_url = std::env::var("BASE_URL")
|
||||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||||
Self { allow_registration, base_url }
|
let rate_limit = std::env::var("RATE_LIMIT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(20);
|
||||||
|
Self { allow_registration, base_url, rate_limit }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(PanicAuth),
|
auth_service: Arc::new(PanicAuth),
|
||||||
password_hasher: Arc::new(PanicHasher),
|
password_hasher: Arc::new(PanicHasher),
|
||||||
user_repository: Arc::new(PanicUserRepo),
|
user_repository: Arc::new(PanicUserRepo),
|
||||||
config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string(), rate_limit: 20 },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(PanicAuth),
|
auth_service: Arc::new(PanicAuth),
|
||||||
password_hasher: Arc::new(PanicHasher),
|
password_hasher: Arc::new(PanicHasher),
|
||||||
user_repository: Arc::new(PanicUserRepo),
|
user_repository: Arc::new(PanicUserRepo),
|
||||||
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string(), rate_limit: 20 },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(PanicRenderer),
|
html_renderer: Arc::new(PanicRenderer),
|
||||||
rss_renderer: Arc::new(PanicRssRenderer),
|
rss_renderer: Arc::new(PanicRssRenderer),
|
||||||
@@ -282,7 +282,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(PanicAuth2),
|
auth_service: Arc::new(PanicAuth2),
|
||||||
password_hasher: Arc::new(PanicHasher2),
|
password_hasher: Arc::new(PanicHasher2),
|
||||||
user_repository: Arc::new(PanicUserRepo2),
|
user_repository: Arc::new(PanicUserRepo2),
|
||||||
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string(), rate_limit: 20 },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(PanicRenderer2),
|
html_renderer: Arc::new(PanicRenderer2),
|
||||||
rss_renderer: Arc::new(PanicRssRenderer2),
|
rss_renderer: Arc::new(PanicRssRenderer2),
|
||||||
@@ -341,7 +341,7 @@ mod tests {
|
|||||||
auth_service: Arc::new(RejectingAuth),
|
auth_service: Arc::new(RejectingAuth),
|
||||||
password_hasher: Arc::new(PanicHasher3),
|
password_hasher: Arc::new(PanicHasher3),
|
||||||
user_repository: Arc::new(PanicUserRepo3),
|
user_repository: Arc::new(PanicUserRepo3),
|
||||||
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
config: application::config::AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string(), rate_limit: 20 },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(PanicRenderer3),
|
html_renderer: Arc::new(PanicRenderer3),
|
||||||
rss_renderer: Arc::new(PanicRssRenderer3),
|
rss_renderer: Arc::new(PanicRssRenderer3),
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ 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)]
|
||||||
@@ -48,17 +46,17 @@ impl RateLimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_router(state: AppState) -> Router {
|
pub fn build_router(state: AppState) -> Router {
|
||||||
|
let rate_limit = state.app_ctx.config.rate_limit;
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(html_routes())
|
.merge(html_routes(rate_limit))
|
||||||
.merge(api_routes())
|
.merge(api_routes(rate_limit))
|
||||||
.nest_service("/static", ServeDir::new("static"))
|
.nest_service("/static", ServeDir::new("static"))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn html_routes() -> Router<AppState> {
|
fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||||
// Auth routes: 20 requests per minute globally.
|
let limiter = RateLimiter::new(rate_limit);
|
||||||
let limiter = RateLimiter::new(API_RATE_LIMIT);
|
|
||||||
let auth = Router::new()
|
let auth = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/login",
|
"/login",
|
||||||
@@ -110,8 +108,8 @@ fn html_routes() -> Router<AppState> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_routes() -> Router<AppState> {
|
fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||||
let limiter = RateLimiter::new(API_RATE_LIMIT);
|
let limiter = RateLimiter::new(rate_limit);
|
||||||
let auth_rate_limit =
|
let auth_rate_limit =
|
||||||
middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| {
|
middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| {
|
||||||
let limiter = limiter.clone();
|
let limiter = limiter.clone();
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ async fn test_app() -> Router {
|
|||||||
auth_service: Arc::new(PanicAuth),
|
auth_service: Arc::new(PanicAuth),
|
||||||
password_hasher: Arc::new(PanicHasher),
|
password_hasher: Arc::new(PanicHasher),
|
||||||
user_repository: Arc::new(NobodyUserRepo),
|
user_repository: Arc::new(NobodyUserRepo),
|
||||||
config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string() },
|
config: AppConfig { allow_registration: false, base_url: "http://localhost:3000".to_string(), rate_limit: 20 },
|
||||||
},
|
},
|
||||||
html_renderer: Arc::new(AskamaHtmlRenderer::new()),
|
html_renderer: Arc::new(AskamaHtmlRenderer::new()),
|
||||||
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
|
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
|
||||||
|
|||||||
Reference in New Issue
Block a user