pinakes-server: wire backup, session refresh, webhooks, and rate limit config
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If2855d44cc700c0f65a5f5ac850ee3866a6a6964
This commit is contained in:
parent
4e91cb6679
commit
52f0b5defc
8 changed files with 257 additions and 105 deletions
|
|
@ -18,62 +18,69 @@ use tower_http::{
|
|||
use crate::{auth, routes, state::AppState};
|
||||
|
||||
/// Create the router with optional TLS configuration for HSTS headers
|
||||
pub fn create_router(state: AppState) -> Router {
|
||||
create_router_with_tls(state, None)
|
||||
pub fn create_router(
|
||||
state: AppState,
|
||||
rate_limits: &pinakes_core::config::RateLimitConfig,
|
||||
) -> Router {
|
||||
create_router_with_tls(state, rate_limits, None)
|
||||
}
|
||||
|
||||
/// Build a governor rate limiter from per-second and burst-size values.
|
||||
/// Panics if the config is invalid (callers must validate before use).
|
||||
fn build_governor(
|
||||
per_second: u64,
|
||||
burst_size: u32,
|
||||
) -> Arc<
|
||||
tower_governor::governor::GovernorConfig<
|
||||
tower_governor::key_extractor::PeerIpKeyExtractor,
|
||||
governor::middleware::NoOpMiddleware,
|
||||
>,
|
||||
> {
|
||||
Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(per_second)
|
||||
.burst_size(burst_size)
|
||||
.finish()
|
||||
.expect("rate limit config was validated at startup"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create the router with TLS configuration for security headers
|
||||
pub fn create_router_with_tls(
|
||||
state: AppState,
|
||||
rate_limits: &pinakes_core::config::RateLimitConfig,
|
||||
tls_config: Option<&pinakes_core::config::TlsConfig>,
|
||||
) -> Router {
|
||||
// Global rate limit: 100 requests/sec per IP
|
||||
let global_governor = Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(1)
|
||||
.burst_size(100)
|
||||
.finish()
|
||||
.expect("valid global rate limit config"),
|
||||
let global_governor = build_governor(
|
||||
rate_limits.global_per_second,
|
||||
rate_limits.global_burst_size,
|
||||
);
|
||||
|
||||
// Strict rate limit for login: 5 requests/min per IP
|
||||
let login_governor = Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(12) // replenish one every 12 seconds
|
||||
.burst_size(5)
|
||||
.finish()
|
||||
.expect("valid login rate limit config"),
|
||||
let login_governor =
|
||||
build_governor(rate_limits.login_per_second, rate_limits.login_burst_size);
|
||||
let search_governor = build_governor(
|
||||
rate_limits.search_per_second,
|
||||
rate_limits.search_burst_size,
|
||||
);
|
||||
|
||||
// Rate limit for search: 10 requests/min per IP
|
||||
let search_governor = Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(6) // replenish one every 6 seconds (10/min)
|
||||
.burst_size(10)
|
||||
.finish()
|
||||
.expect("valid search rate limit config"),
|
||||
);
|
||||
|
||||
// Rate limit for streaming: 5 requests per IP (very restrictive for
|
||||
// concurrent streams)
|
||||
let stream_governor = Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(60) // replenish slowly (one per minute)
|
||||
.burst_size(5) // max 5 concurrent connections
|
||||
.finish()
|
||||
.expect("valid stream rate limit config"),
|
||||
let stream_governor = build_governor(
|
||||
rate_limits.stream_per_second,
|
||||
rate_limits.stream_burst_size,
|
||||
);
|
||||
let share_governor =
|
||||
build_governor(rate_limits.share_per_second, rate_limits.share_burst_size);
|
||||
|
||||
// Login route with strict rate limiting
|
||||
let login_route = Router::new()
|
||||
.route("/auth/login", post(routes::auth::login))
|
||||
.layer(GovernorLayer::new(login_governor));
|
||||
|
||||
// Share routes with dedicated rate limiting
|
||||
let share_routes = Router::new()
|
||||
.route("/s/{token}", get(routes::social::access_shared_media))
|
||||
.route("/shared/{token}", get(routes::shares::access_shared))
|
||||
.layer(GovernorLayer::new(share_governor));
|
||||
|
||||
// Public routes (no auth required)
|
||||
let public_routes = Router::new()
|
||||
.route("/s/{token}", get(routes::social::access_shared_media))
|
||||
// Enhanced sharing: public share access
|
||||
.route("/shared/{token}", get(routes::shares::access_shared))
|
||||
// Kubernetes-style health probes (no auth required for orchestration)
|
||||
.route("/health/live", get(routes::health::liveness))
|
||||
.route("/health/ready", get(routes::health::readiness));
|
||||
|
|
@ -139,6 +146,7 @@ pub fn create_router_with_tls(
|
|||
// Auth endpoints (self-service); login is handled separately with a stricter rate limit
|
||||
.route("/auth/logout", post(routes::auth::logout))
|
||||
.route("/auth/me", get(routes::auth::me))
|
||||
.route("/auth/refresh", post(routes::auth::refresh))
|
||||
.route("/auth/revoke-all", post(routes::auth::revoke_all_sessions))
|
||||
// Social: ratings & comments (read)
|
||||
.route(
|
||||
|
|
@ -469,6 +477,7 @@ pub fn create_router_with_tls(
|
|||
.route("/config/ui", put(routes::config::update_ui_config))
|
||||
.route("/database/vacuum", post(routes::database::vacuum_database))
|
||||
.route("/database/clear", post(routes::database::clear_database))
|
||||
.route("/database/backup", post(routes::backup::create_backup))
|
||||
// Plugin management
|
||||
.route("/plugins", get(routes::plugins::list_plugins))
|
||||
.route("/plugins/{id}", get(routes::plugins::get_plugin))
|
||||
|
|
@ -498,22 +507,47 @@ pub fn create_router_with_tls(
|
|||
.route("/auth/sessions", get(routes::auth::list_active_sessions))
|
||||
.layer(middleware::from_fn(auth::require_admin));
|
||||
|
||||
// CORS: allow same-origin by default, plus the desktop UI origin
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin([
|
||||
HeaderValue::from_static("http://localhost:3000"),
|
||||
HeaderValue::from_static("http://127.0.0.1:3000"),
|
||||
HeaderValue::from_static("tauri://localhost"),
|
||||
])
|
||||
.allow_methods([
|
||||
Method::GET,
|
||||
Method::POST,
|
||||
Method::PUT,
|
||||
Method::PATCH,
|
||||
Method::DELETE,
|
||||
])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
|
||||
.allow_credentials(true);
|
||||
// CORS configuration: use config-driven origins if specified,
|
||||
// otherwise fall back to default localhost origins
|
||||
let cors = {
|
||||
let origins: Vec<HeaderValue> =
|
||||
if let Ok(config_read) = state.config.try_read() {
|
||||
if config_read.server.cors_enabled
|
||||
&& !config_read.server.cors_origins.is_empty()
|
||||
{
|
||||
config_read
|
||||
.server
|
||||
.cors_origins
|
||||
.iter()
|
||||
.filter_map(|o| HeaderValue::from_str(o).ok())
|
||||
.collect()
|
||||
} else {
|
||||
vec![
|
||||
HeaderValue::from_static("http://localhost:3000"),
|
||||
HeaderValue::from_static("http://127.0.0.1:3000"),
|
||||
HeaderValue::from_static("tauri://localhost"),
|
||||
]
|
||||
}
|
||||
} else {
|
||||
vec![
|
||||
HeaderValue::from_static("http://localhost:3000"),
|
||||
HeaderValue::from_static("http://127.0.0.1:3000"),
|
||||
HeaderValue::from_static("tauri://localhost"),
|
||||
]
|
||||
};
|
||||
|
||||
CorsLayer::new()
|
||||
.allow_origin(origins)
|
||||
.allow_methods([
|
||||
Method::GET,
|
||||
Method::POST,
|
||||
Method::PUT,
|
||||
Method::PATCH,
|
||||
Method::DELETE,
|
||||
])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
|
||||
.allow_credentials(true)
|
||||
};
|
||||
|
||||
// Create protected routes with auth middleware
|
||||
let protected_api = Router::new()
|
||||
|
|
@ -527,10 +561,11 @@ pub fn create_router_with_tls(
|
|||
auth::require_auth,
|
||||
));
|
||||
|
||||
// Combine protected and public routes
|
||||
// Combine protected, public, and share routes
|
||||
let full_api = Router::new()
|
||||
.merge(login_route)
|
||||
.merge(public_routes)
|
||||
.merge(share_routes)
|
||||
.merge(protected_api);
|
||||
|
||||
// Build security headers layer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue