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:
raf 2026-03-08 00:42:14 +03:00
commit 52f0b5defc
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
8 changed files with 257 additions and 105 deletions

View file

@ -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