From 829e58d0ae9800ef8481b45ce937f01ff465937c Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 13 Mar 2026 13:13:47 +0100 Subject: [PATCH 1/5] Split admin routes and add log downloads --- Cargo.toml | 1 + src/config.rs | 15 +- src/main.rs | 10 +- src/routes/admin/logs.rs | 207 +++++++++++++++++++++ src/routes/admin/mod.rs | 11 ++ src/routes/{admin.rs => admin/registry.rs} | 0 src/test_helpers.rs | 11 +- 7 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/routes/admin/logs.rs create mode 100644 src/routes/admin/mod.rs rename src/routes/{admin.rs => admin/registry.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 267ac21..9185563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ futures = "0.3" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tracing-appender = "0.2" +chrono = { version = "0.4", default-features = false, features = ["std"] } rand = "0.9" uuid = { version = "1", features = ["v4"] } sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "migrate"] } diff --git a/src/config.rs b/src/config.rs index fc3f127..349738e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use serde::Deserialize; -use std::path::Path; +use std::path::{Path, PathBuf}; #[derive(Deserialize)] pub struct Config { @@ -12,6 +12,19 @@ pub struct Config { pub local_db_path: String, } +#[derive(Debug, Clone)] +pub struct LogDirectory(pub PathBuf); + +impl LogDirectory { + pub fn new(path: impl Into) -> Self { + Self(path.into()) + } + + pub fn as_path(&self) -> &Path { + &self.0 + } +} + impl Config { pub fn load(path: &Path) -> Result { let contents = diff --git a/src/main.rs b/src/main.rs index 440ab10..e71c9a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -115,6 +115,7 @@ pub(crate) fn rocket( rate_limiter: fairings::RateLimiter, raindex_config: raindex::SharedRaindexProvider, docs_dir: String, + log_dir: std::path::PathBuf, ) -> Result, StartupError> { let cors = configure_cors()?; @@ -126,6 +127,7 @@ pub(crate) fn rocket( .manage(pool) .manage(rate_limiter) .manage(raindex_config) + .manage(config::LogDirectory::new(log_dir)) .mount("/", routes::health::routes()) .mount("/v1/tokens", routes::tokens::routes()) .mount("/v1/swap", routes::swap::routes()) @@ -261,7 +263,13 @@ async fn main() { } tracing::info!(docs_dir = %cfg.docs_dir, "serving documentation at /docs"); - let rocket = match rocket(pool, rate_limiter, shared_raindex, cfg.docs_dir) { + let rocket = match rocket( + pool, + rate_limiter, + shared_raindex, + cfg.docs_dir, + std::path::PathBuf::from(&cfg.log_dir), + ) { Ok(r) => r, Err(e) => { tracing::error!(error = %e, "failed to build Rocket instance"); diff --git a/src/routes/admin/logs.rs b/src/routes/admin/logs.rs new file mode 100644 index 0000000..0620eee --- /dev/null +++ b/src/routes/admin/logs.rs @@ -0,0 +1,207 @@ +use crate::auth::AdminKey; +use crate::config::LogDirectory; +use crate::error::ApiError; +use crate::fairings::{GlobalRateLimit, TracingSpan}; +use chrono::NaiveDate; +use rocket::form::FromForm; +use rocket::fs::NamedFile; +use rocket::http::{ContentType, Header}; +use rocket::request::Request; +use rocket::response::Responder; +use rocket::{Route, State}; +use std::path::{Path, PathBuf}; +use tracing::Instrument; + +const LOG_FILE_PREFIX: &str = "st0x-rest-api.log"; + +#[derive(Debug, Clone, FromForm)] +pub struct AdminLogDownloadParams { + pub date: String, +} + +pub struct AdminLogDownload { + file: NamedFile, + filename: String, +} + +impl<'r> Responder<'r, 'static> for AdminLogDownload { + fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> { + let mut response = self.file.respond_to(req)?; + response.set_header(ContentType::Binary); + response.set_header(Header::new( + "Content-Disposition", + format!("attachment; filename=\"{}\"", self.filename), + )); + Ok(response) + } +} + +fn parse_log_date(date: &str) -> Result { + NaiveDate::parse_from_str(date, "%Y-%m-%d") + .map_err(|_| ApiError::BadRequest("date must use YYYY-MM-DD format".into())) +} + +fn daily_log_path(log_dir: &Path, date: &NaiveDate) -> PathBuf { + log_dir.join(format!("{LOG_FILE_PREFIX}.{}", date.format("%Y-%m-%d"))) +} + +fn download_filename(date: &NaiveDate) -> String { + format!("st0x-rest-api-{}.log", date.format("%Y-%m-%d")) +} + +#[get("/logs?")] +pub async fn get_logs( + _global: GlobalRateLimit, + _admin: AdminKey, + log_dir: &State, + span: TracingSpan, + params: AdminLogDownloadParams, +) -> Result { + async move { + tracing::info!(date = %params.date, "request received"); + + let date = parse_log_date(¶ms.date)?; + let log_path = daily_log_path(log_dir.as_path(), &date); + let filename = download_filename(&date); + + tracing::info!(path = %log_path.display(), "resolved log file path"); + + let file = NamedFile::open(&log_path).await.map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + tracing::warn!(path = %log_path.display(), "requested log file not found"); + ApiError::NotFound("log file not found".into()) + } else { + tracing::error!(path = %log_path.display(), error = %e, "failed to open log file"); + ApiError::Internal("failed to open log file".into()) + } + })?; + + tracing::info!( + path = %log_path.display(), + filename = %filename, + "serving log file" + ); + + Ok(AdminLogDownload { file, filename }) + } + .instrument(span.0) + .await +} + +pub fn routes() -> Vec { + rocket::routes![get_logs] +} + +#[cfg(test)] +mod tests { + use crate::test_helpers::{basic_auth_header, seed_admin_key, seed_api_key, TestClientBuilder}; + use rocket::http::{ContentType, Header, Status}; + use tempfile::TempDir; + + fn write_log_file(temp_dir: &TempDir, date: &str, contents: &str) { + let path = temp_dir.path().join(format!("st0x-rest-api.log.{date}")); + std::fs::write(path, contents).expect("write log file"); + } + + #[rocket::async_test] + async fn test_get_logs_with_admin_key() { + let temp_dir = TempDir::new().expect("temp dir"); + write_log_file(&temp_dir, "2026-03-13", "line one\nline two\n"); + + let client = TestClientBuilder::new() + .log_dir(temp_dir.path()) + .build() + .await; + let (key_id, secret) = seed_admin_key(&client).await; + let header = basic_auth_header(&key_id, &secret); + + let response = client + .get("/admin/logs?date=2026-03-13") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::Binary)); + assert_eq!( + response.headers().get_one("Content-Disposition"), + Some("attachment; filename=\"st0x-rest-api-2026-03-13.log\"") + ); + assert_eq!( + response.into_string().await.unwrap(), + "line one\nline two\n".to_string() + ); + } + + #[rocket::async_test] + async fn test_get_logs_with_non_admin_key_returns_403() { + let temp_dir = TempDir::new().expect("temp dir"); + write_log_file(&temp_dir, "2026-03-13", "line one\n"); + + let client = TestClientBuilder::new() + .log_dir(temp_dir.path()) + .build() + .await; + let (key_id, secret) = seed_api_key(&client).await; + let header = basic_auth_header(&key_id, &secret); + + let response = client + .get("/admin/logs?date=2026-03-13") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Forbidden); + } + + #[rocket::async_test] + async fn test_get_logs_without_auth_returns_401() { + let client = TestClientBuilder::new().build().await; + + let response = client.get("/admin/logs?date=2026-03-13").dispatch().await; + + assert_eq!(response.status(), Status::Unauthorized); + } + + #[rocket::async_test] + async fn test_get_logs_with_invalid_date_returns_400() { + let client = TestClientBuilder::new().build().await; + let (key_id, secret) = seed_admin_key(&client).await; + let header = basic_auth_header(&key_id, &secret); + + let response = client + .get("/admin/logs?date=2026-13-40") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + + assert_eq!(response.status(), Status::BadRequest); + let body: serde_json::Value = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(body["error"]["code"], "BAD_REQUEST"); + assert_eq!(body["error"]["message"], "date must use YYYY-MM-DD format"); + } + + #[rocket::async_test] + async fn test_get_logs_when_file_is_missing_returns_404() { + let temp_dir = TempDir::new().expect("temp dir"); + let client = TestClientBuilder::new() + .log_dir(temp_dir.path()) + .build() + .await; + let (key_id, secret) = seed_admin_key(&client).await; + let header = basic_auth_header(&key_id, &secret); + + let response = client + .get("/admin/logs?date=2026-03-13") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NotFound); + let body: serde_json::Value = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(body["error"]["code"], "NOT_FOUND"); + assert_eq!(body["error"]["message"], "log file not found"); + } +} diff --git a/src/routes/admin/mod.rs b/src/routes/admin/mod.rs new file mode 100644 index 0000000..7a860f8 --- /dev/null +++ b/src/routes/admin/mod.rs @@ -0,0 +1,11 @@ +mod logs; +mod registry; + +use rocket::Route; + +pub fn routes() -> Vec { + let mut routes = Vec::new(); + routes.extend(registry::routes()); + routes.extend(logs::routes()); + routes +} diff --git a/src/routes/admin.rs b/src/routes/admin/registry.rs similarity index 100% rename from src/routes/admin.rs rename to src/routes/admin/registry.rs diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 9c046a2..e5fd577 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -6,6 +6,7 @@ use rain_orderbook_common::raindex_client::orders::RaindexOrder; use rain_orderbook_common::take_orders::TakeOrderCandidate; use rocket::local::asynchronous::Client; use serde_json::json; +use std::path::PathBuf; pub(crate) async fn client() -> Client { TestClientBuilder::new().build().await @@ -15,6 +16,7 @@ pub(crate) struct TestClientBuilder { rate_limiter: crate::fairings::RateLimiter, raindex_registry_url: Option, raindex_config: Option, + log_dir: Option, } impl TestClientBuilder { @@ -23,6 +25,7 @@ impl TestClientBuilder { rate_limiter: crate::fairings::RateLimiter::new(10000, 10000), raindex_registry_url: None, raindex_config: None, + log_dir: None, } } @@ -36,6 +39,11 @@ impl TestClientBuilder { self } + pub(crate) fn log_dir(mut self, log_dir: impl Into) -> Self { + self.log_dir = Some(log_dir.into()); + self + } + pub(crate) async fn build(self) -> Client { let id = uuid::Uuid::new_v4(); let pool = crate::db::init(&format!("sqlite:file:{id}?mode=memory&cache=shared")) @@ -57,7 +65,8 @@ impl TestClientBuilder { let shared_raindex = tokio::sync::RwLock::new(raindex_config); let docs_dir = std::env::temp_dir().to_string_lossy().into_owned(); - let rocket = crate::rocket(pool, self.rate_limiter, shared_raindex, docs_dir) + let log_dir = self.log_dir.unwrap_or_else(std::env::temp_dir); + let rocket = crate::rocket(pool, self.rate_limiter, shared_raindex, docs_dir, log_dir) .expect("valid rocket instance"); Client::tracked(rocket).await.expect("valid client") From 1e899afb245be20d7b26cb16d11c369cad2bbf31 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 13 Mar 2026 13:17:24 +0100 Subject: [PATCH 2/5] Introduce typed path config for logs --- src/config.rs | 19 ++----------- src/log_files.rs | 60 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 17 ++++++------ src/routes/admin/logs.rs | 26 ++++++----------- src/telemetry.rs | 5 ++-- src/test_helpers.rs | 2 +- 6 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 src/log_files.rs diff --git a/src/config.rs b/src/config.rs index 349738e..5cfb157 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,26 +3,13 @@ use std::path::{Path, PathBuf}; #[derive(Deserialize)] pub struct Config { - pub log_dir: String, + pub log_dir: PathBuf, pub database_url: String, pub registry_url: String, pub rate_limit_global_rpm: u64, pub rate_limit_per_key_rpm: u64, - pub docs_dir: String, - pub local_db_path: String, -} - -#[derive(Debug, Clone)] -pub struct LogDirectory(pub PathBuf); - -impl LogDirectory { - pub fn new(path: impl Into) -> Self { - Self(path.into()) - } - - pub fn as_path(&self) -> &Path { - &self.0 - } + pub docs_dir: PathBuf, + pub local_db_path: PathBuf, } impl Config { diff --git a/src/log_files.rs b/src/log_files.rs new file mode 100644 index 0000000..ab71016 --- /dev/null +++ b/src/log_files.rs @@ -0,0 +1,60 @@ +use chrono::NaiveDate; +use std::path::{Path, PathBuf}; + +const LOG_FILE_PREFIX: &str = "st0x-rest-api.log"; + +#[derive(Debug, Clone)] +pub struct LogFiles { + root: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct LogFile { + path: PathBuf, + download_filename: String, +} + +impl LogFiles { + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } + + pub fn file_for_date(&self, date: NaiveDate) -> LogFile { + let formatted_date = date.format("%Y-%m-%d"); + LogFile { + path: self + .root + .join(format!("{LOG_FILE_PREFIX}.{formatted_date}")), + download_filename: format!("st0x-rest-api-{formatted_date}.log"), + } + } +} + +impl LogFile { + pub fn path(&self) -> &Path { + &self.path + } + + pub fn download_filename(&self) -> &str { + &self.download_filename + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_expected_daily_log_path_and_filename() { + let logs = LogFiles::new("/tmp/st0x-logs"); + let date = NaiveDate::from_ymd_opt(2026, 3, 13).expect("valid test date"); + + let file = logs.file_for_date(date); + + assert_eq!( + file.path(), + Path::new("/tmp/st0x-logs/st0x-rest-api.log.2026-03-13") + ); + assert_eq!(file.download_filename(), "st0x-rest-api-2026-03-13.log"); + } +} diff --git a/src/main.rs b/src/main.rs index e71c9a8..6e57715 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod config; mod db; mod error; mod fairings; +mod log_files; mod raindex; mod routes; mod telemetry; @@ -114,7 +115,7 @@ pub(crate) fn rocket( pool: db::DbPool, rate_limiter: fairings::RateLimiter, raindex_config: raindex::SharedRaindexProvider, - docs_dir: String, + docs_dir: std::path::PathBuf, log_dir: std::path::PathBuf, ) -> Result, StartupError> { let cors = configure_cors()?; @@ -127,7 +128,7 @@ pub(crate) fn rocket( .manage(pool) .manage(rate_limiter) .manage(raindex_config) - .manage(config::LogDirectory::new(log_dir)) + .manage(log_files::LogFiles::new(log_dir)) .mount("/", routes::health::routes()) .mount("/v1/tokens", routes::tokens::routes()) .mount("/v1/swap", routes::swap::routes()) @@ -224,7 +225,7 @@ async fn main() { } }; - let local_db_path = std::path::PathBuf::from(&cfg.local_db_path); + let local_db_path = cfg.local_db_path.clone(); if let Some(parent) = local_db_path.parent() { if !parent.exists() { if let Err(e) = std::fs::create_dir_all(parent) { @@ -256,19 +257,19 @@ async fn main() { let rate_limiter = fairings::RateLimiter::new(cfg.rate_limit_global_rpm, cfg.rate_limit_per_key_rpm); - if !std::path::Path::new(&cfg.docs_dir).is_dir() { - tracing::error!(docs_dir = %cfg.docs_dir, "docs_dir is not a valid directory"); + if !cfg.docs_dir.is_dir() { + tracing::error!(docs_dir = %cfg.docs_dir.display(), "docs_dir is not a valid directory"); drop(log_guard); std::process::exit(1); } - tracing::info!(docs_dir = %cfg.docs_dir, "serving documentation at /docs"); + tracing::info!(docs_dir = %cfg.docs_dir.display(), "serving documentation at /docs"); let rocket = match rocket( pool, rate_limiter, shared_raindex, - cfg.docs_dir, - std::path::PathBuf::from(&cfg.log_dir), + cfg.docs_dir.clone(), + cfg.log_dir.clone(), ) { Ok(r) => r, Err(e) => { diff --git a/src/routes/admin/logs.rs b/src/routes/admin/logs.rs index 0620eee..623774e 100644 --- a/src/routes/admin/logs.rs +++ b/src/routes/admin/logs.rs @@ -1,7 +1,7 @@ use crate::auth::AdminKey; -use crate::config::LogDirectory; use crate::error::ApiError; use crate::fairings::{GlobalRateLimit, TracingSpan}; +use crate::log_files::LogFiles; use chrono::NaiveDate; use rocket::form::FromForm; use rocket::fs::NamedFile; @@ -9,11 +9,8 @@ use rocket::http::{ContentType, Header}; use rocket::request::Request; use rocket::response::Responder; use rocket::{Route, State}; -use std::path::{Path, PathBuf}; use tracing::Instrument; -const LOG_FILE_PREFIX: &str = "st0x-rest-api.log"; - #[derive(Debug, Clone, FromForm)] pub struct AdminLogDownloadParams { pub date: String, @@ -41,19 +38,11 @@ fn parse_log_date(date: &str) -> Result { .map_err(|_| ApiError::BadRequest("date must use YYYY-MM-DD format".into())) } -fn daily_log_path(log_dir: &Path, date: &NaiveDate) -> PathBuf { - log_dir.join(format!("{LOG_FILE_PREFIX}.{}", date.format("%Y-%m-%d"))) -} - -fn download_filename(date: &NaiveDate) -> String { - format!("st0x-rest-api-{}.log", date.format("%Y-%m-%d")) -} - #[get("/logs?")] pub async fn get_logs( _global: GlobalRateLimit, _admin: AdminKey, - log_dir: &State, + log_files: &State, span: TracingSpan, params: AdminLogDownloadParams, ) -> Result { @@ -61,8 +50,8 @@ pub async fn get_logs( tracing::info!(date = %params.date, "request received"); let date = parse_log_date(¶ms.date)?; - let log_path = daily_log_path(log_dir.as_path(), &date); - let filename = download_filename(&date); + let log_file = log_files.file_for_date(date); + let log_path = log_file.path().to_path_buf(); tracing::info!(path = %log_path.display(), "resolved log file path"); @@ -78,11 +67,14 @@ pub async fn get_logs( tracing::info!( path = %log_path.display(), - filename = %filename, + filename = %log_file.download_filename(), "serving log file" ); - Ok(AdminLogDownload { file, filename }) + Ok(AdminLogDownload { + file, + filename: log_file.download_filename().to_string(), + }) } .instrument(span.0) .await diff --git a/src/telemetry.rs b/src/telemetry.rs index 31a9d52..b7bcc2d 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,12 +1,13 @@ +use std::path::Path; use std::sync::Once; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; static TELEMETRY_INIT: Once = Once::new(); -pub fn init(log_dir: &str) -> Result { +pub fn init(log_dir: &Path) -> Result { let mut guard_slot: Option = None; - let log_dir = log_dir.to_string(); + let log_dir = log_dir.to_path_buf(); TELEMETRY_INIT.call_once(|| { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|e| { diff --git a/src/test_helpers.rs b/src/test_helpers.rs index e5fd577..d6466b9 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -64,7 +64,7 @@ impl TestClientBuilder { }; let shared_raindex = tokio::sync::RwLock::new(raindex_config); - let docs_dir = std::env::temp_dir().to_string_lossy().into_owned(); + let docs_dir = std::env::temp_dir(); let log_dir = self.log_dir.unwrap_or_else(std::env::temp_dir); let rocket = crate::rocket(pool, self.rate_limiter, shared_raindex, docs_dir, log_dir) .expect("valid rocket instance"); From b32f969a05d9beeef8d51149b0dd971c943d1aea Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 13 Mar 2026 13:30:26 +0100 Subject: [PATCH 3/5] Update Cargo.lock --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 2e9f76f..54d6c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8919,6 +8919,7 @@ dependencies = [ "argon2", "async-trait", "base64 0.22.1", + "chrono", "clap", "futures", "rain-math-float", From 227f2087aae2c192b86fd8a1b4455bb4f76c9c11 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 13 Mar 2026 13:49:07 +0100 Subject: [PATCH 4/5] Refine admin log handling --- src/log_files.rs | 4 ++-- src/routes/admin/logs.rs | 36 +++++++++++++++++++++++++++++++----- src/routes/admin/registry.rs | 19 ++----------------- src/telemetry.rs | 3 ++- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/log_files.rs b/src/log_files.rs index ab71016..e018076 100644 --- a/src/log_files.rs +++ b/src/log_files.rs @@ -1,7 +1,7 @@ use chrono::NaiveDate; use std::path::{Path, PathBuf}; -const LOG_FILE_PREFIX: &str = "st0x-rest-api.log"; +pub const LOG_FILE_BASENAME: &str = "st0x-rest-api.log"; #[derive(Debug, Clone)] pub struct LogFiles { @@ -24,7 +24,7 @@ impl LogFiles { LogFile { path: self .root - .join(format!("{LOG_FILE_PREFIX}.{formatted_date}")), + .join(format!("{LOG_FILE_BASENAME}.{formatted_date}")), download_filename: format!("st0x-rest-api-{formatted_date}.log"), } } diff --git a/src/routes/admin/logs.rs b/src/routes/admin/logs.rs index 623774e..acbcc34 100644 --- a/src/routes/admin/logs.rs +++ b/src/routes/admin/logs.rs @@ -13,7 +13,7 @@ use tracing::Instrument; #[derive(Debug, Clone, FromForm)] pub struct AdminLogDownloadParams { - pub date: String, + pub date: Option, } pub struct AdminLogDownload { @@ -33,7 +33,12 @@ impl<'r> Responder<'r, 'static> for AdminLogDownload { } } -fn parse_log_date(date: &str) -> Result { +fn parse_log_date(params: &AdminLogDownloadParams) -> Result { + let date = params + .date + .as_deref() + .ok_or_else(|| ApiError::BadRequest("date query parameter is required".into()))?; + NaiveDate::parse_from_str(date, "%Y-%m-%d") .map_err(|_| ApiError::BadRequest("date must use YYYY-MM-DD format".into())) } @@ -47,9 +52,9 @@ pub async fn get_logs( params: AdminLogDownloadParams, ) -> Result { async move { - tracing::info!(date = %params.date, "request received"); + tracing::info!(date = ?params.date, "request received"); - let date = parse_log_date(¶ms.date)?; + let date = parse_log_date(¶ms)?; let log_file = log_files.file_for_date(date); let log_path = log_file.path().to_path_buf(); @@ -91,7 +96,9 @@ mod tests { use tempfile::TempDir; fn write_log_file(temp_dir: &TempDir, date: &str, contents: &str) { - let path = temp_dir.path().join(format!("st0x-rest-api.log.{date}")); + let path = temp_dir + .path() + .join(format!("{}.{date}", crate::log_files::LOG_FILE_BASENAME)); std::fs::write(path, contents).expect("write log file"); } @@ -174,6 +181,25 @@ mod tests { assert_eq!(body["error"]["message"], "date must use YYYY-MM-DD format"); } + #[rocket::async_test] + async fn test_get_logs_without_date_returns_400() { + let client = TestClientBuilder::new().build().await; + let (key_id, secret) = seed_admin_key(&client).await; + let header = basic_auth_header(&key_id, &secret); + + let response = client + .get("/admin/logs") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + + assert_eq!(response.status(), Status::BadRequest); + let body: serde_json::Value = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(body["error"]["code"], "BAD_REQUEST"); + assert_eq!(body["error"]["message"], "date query parameter is required"); + } + #[rocket::async_test] async fn test_get_logs_when_file_is_missing_returns_404() { let temp_dir = TempDir::new().expect("temp dir"); diff --git a/src/routes/admin/registry.rs b/src/routes/admin/registry.rs index 0313136..c8a715e 100644 --- a/src/routes/admin/registry.rs +++ b/src/routes/admin/registry.rs @@ -1,6 +1,6 @@ use crate::auth::AdminKey; use crate::db::{settings, DbPool}; -use crate::error::{ApiError, ApiErrorResponse}; +use crate::error::ApiError; use crate::fairings::{GlobalRateLimit, TracingSpan}; use crate::raindex::{RaindexProvider, SharedRaindexProvider}; use crate::routes::registry::RegistryResponse; @@ -8,27 +8,12 @@ use rocket::serde::json::Json; use rocket::{Route, State}; use serde::{Deserialize, Serialize}; use tracing::Instrument; -use utoipa::ToSchema; -#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Serialize, Deserialize)] pub struct UpdateRegistryRequest { pub registry_url: String, } -#[utoipa::path( - put, - path = "/admin/registry", - tag = "Admin", - security(("basicAuth" = [])), - request_body = UpdateRegistryRequest, - responses( - (status = 200, description = "Registry updated", body = RegistryResponse), - (status = 400, description = "Bad request", body = ApiErrorResponse), - (status = 401, description = "Unauthorized", body = ApiErrorResponse), - (status = 403, description = "Forbidden", body = ApiErrorResponse), - (status = 500, description = "Internal server error", body = ApiErrorResponse), - ) -)] #[put("/registry", data = "")] pub async fn put_registry( _global: GlobalRateLimit, diff --git a/src/telemetry.rs b/src/telemetry.rs index b7bcc2d..1f2b9fc 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,3 +1,4 @@ +use crate::log_files::LOG_FILE_BASENAME; use std::path::Path; use std::sync::Once; use tracing_appender::non_blocking::WorkerGuard; @@ -14,7 +15,7 @@ pub fn init(log_dir: &Path) -> Result { eprintln!("invalid RUST_LOG filter, using default: {e}"); EnvFilter::new("st0x_rest_api=info,rocket=warn,warn") }); - let file_appender = tracing_appender::rolling::daily(&log_dir, "st0x-rest-api.log"); + let file_appender = tracing_appender::rolling::daily(&log_dir, LOG_FILE_BASENAME); let (file_writer, file_guard) = tracing_appender::non_blocking(file_appender); let init_result = tracing_subscriber::registry() From 2f39983735c3200db9d9ac22dac8e2bb772cbb13 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 13 Mar 2026 14:17:18 +0100 Subject: [PATCH 5/5] Log rejected admin log dates --- src/routes/admin/logs.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/routes/admin/logs.rs b/src/routes/admin/logs.rs index acbcc34..deeb886 100644 --- a/src/routes/admin/logs.rs +++ b/src/routes/admin/logs.rs @@ -34,13 +34,17 @@ impl<'r> Responder<'r, 'static> for AdminLogDownload { } fn parse_log_date(params: &AdminLogDownloadParams) -> Result { - let date = params - .date - .as_deref() - .ok_or_else(|| ApiError::BadRequest("date query parameter is required".into()))?; + let Some(date) = params.date.as_deref() else { + tracing::warn!("missing required date query parameter"); + return Err(ApiError::BadRequest( + "date query parameter is required".into(), + )); + }; - NaiveDate::parse_from_str(date, "%Y-%m-%d") - .map_err(|_| ApiError::BadRequest("date must use YYYY-MM-DD format".into())) + NaiveDate::parse_from_str(date, "%Y-%m-%d").map_err(|_| { + tracing::warn!(date = %date, "rejected invalid log download date"); + ApiError::BadRequest("date must use YYYY-MM-DD format".into()) + }) } #[get("/logs?")]