diff --git a/apps/api/src/books.rs b/apps/api/src/books.rs index 2ebc11d..11f4ca4 100644 --- a/apps/api/src/books.rs +++ b/apps/api/src/books.rs @@ -347,6 +347,21 @@ use axum::{ response::IntoResponse, }; +/// Get book thumbnail image +#[utoipa::path( + get, + path = "/books/{id}/thumbnail", + tag = "books", + params( + ("id" = String, Path, description = "Book UUID"), + ), + responses( + (status = 200, description = "WebP thumbnail image", content_type = "image/webp"), + (status = 404, description = "Book not found or thumbnail not available"), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] pub async fn get_thumbnail( State(state): State, Path(book_id): Path, diff --git a/apps/api/src/openapi.rs b/apps/api/src/openapi.rs index 6d1c6fc..6cde81e 100644 --- a/apps/api/src/openapi.rs +++ b/apps/api/src/openapi.rs @@ -6,6 +6,7 @@ use utoipa::OpenApi; paths( crate::books::list_books, crate::books::get_book, + crate::books::get_thumbnail, crate::books::list_series, crate::pages::get_page, crate::search::search_books, @@ -27,6 +28,12 @@ use utoipa::OpenApi; crate::tokens::list_tokens, crate::tokens::create_token, crate::tokens::revoke_token, + crate::settings::get_settings, + crate::settings::get_setting, + crate::settings::update_setting, + crate::settings::clear_cache, + crate::settings::get_cache_stats, + crate::settings::get_thumbnail_stats, ), components( schemas( @@ -51,6 +58,10 @@ use utoipa::OpenApi; crate::tokens::CreateTokenRequest, crate::tokens::TokenResponse, crate::tokens::CreatedTokenResponse, + crate::settings::UpdateSettingRequest, + crate::settings::ClearCacheResponse, + crate::settings::CacheStats, + crate::settings::ThumbnailStats, ErrorResponse, ) ), @@ -62,6 +73,7 @@ use utoipa::OpenApi; (name = "libraries", description = "Library management endpoints (Admin only)"), (name = "indexing", description = "Search index management and job control (Admin only)"), (name = "tokens", description = "API token management (Admin only)"), + (name = "settings", description = "Application settings and cache management (Admin only)"), ), modifiers(&SecurityAddon) )] diff --git a/apps/api/src/settings.rs b/apps/api/src/settings.rs index a5aaf1b..855d6b2 100644 --- a/apps/api/src/settings.rs +++ b/apps/api/src/settings.rs @@ -6,28 +6,29 @@ use axum::{ use serde::{Deserialize, Serialize}; use serde_json::Value; use sqlx::Row; +use utoipa::ToSchema; use crate::{error::ApiError, state::AppState}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct UpdateSettingRequest { pub value: Value, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ClearCacheResponse { pub success: bool, pub message: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct CacheStats { pub total_size_mb: f64, pub file_count: u64, pub directory: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ThumbnailStats { pub total_size_mb: f64, pub file_count: u64, @@ -43,7 +44,18 @@ pub fn settings_routes() -> Router { .route("/settings/thumbnail/stats", get(get_thumbnail_stats)) } -async fn get_settings(State(state): State) -> Result, ApiError> { +/// List all settings +#[utoipa::path( + get, + path = "/settings", + tag = "settings", + responses( + (status = 200, description = "All settings as key/value object"), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] +pub async fn get_settings(State(state): State) -> Result, ApiError> { let rows = sqlx::query(r#"SELECT key, value FROM app_settings"#) .fetch_all(&state.pool) .await?; @@ -58,7 +70,20 @@ async fn get_settings(State(state): State) -> Result, ApiE Ok(Json(Value::Object(settings))) } -async fn get_setting( +/// Get a single setting by key +#[utoipa::path( + get, + path = "/settings/{key}", + tag = "settings", + params(("key" = String, Path, description = "Setting key")), + responses( + (status = 200, description = "Setting value"), + (status = 404, description = "Setting not found"), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] +pub async fn get_setting( State(state): State, axum::extract::Path(key): axum::extract::Path, ) -> Result, ApiError> { @@ -76,7 +101,20 @@ async fn get_setting( } } -async fn update_setting( +/// Create or update a setting +#[utoipa::path( + post, + path = "/settings/{key}", + tag = "settings", + params(("key" = String, Path, description = "Setting key")), + request_body = UpdateSettingRequest, + responses( + (status = 200, description = "Updated setting value"), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] +pub async fn update_setting( State(state): State, axum::extract::Path(key): axum::extract::Path, Json(body): Json, @@ -99,7 +137,18 @@ async fn update_setting( Ok(Json(value)) } -async fn clear_cache(State(_state): State) -> Result, ApiError> { +/// Clear the image page cache +#[utoipa::path( + post, + path = "/settings/cache/clear", + tag = "settings", + responses( + (status = 200, body = ClearCacheResponse), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] +pub async fn clear_cache(State(_state): State) -> Result, ApiError> { let cache_dir = std::env::var("IMAGE_CACHE_DIR") .unwrap_or_else(|_| "/tmp/stripstream-image-cache".to_string()); @@ -128,7 +177,18 @@ async fn clear_cache(State(_state): State) -> Result) -> Result, ApiError> { +/// Get image page cache statistics +#[utoipa::path( + get, + path = "/settings/cache/stats", + tag = "settings", + responses( + (status = 200, body = CacheStats), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] +pub async fn get_cache_stats(State(_state): State) -> Result, ApiError> { let cache_dir = std::env::var("IMAGE_CACHE_DIR") .unwrap_or_else(|_| "/tmp/stripstream-image-cache".to_string()); @@ -208,7 +268,18 @@ fn compute_dir_stats(path: &std::path::Path) -> (u64, u64) { (total_size, file_count) } -async fn get_thumbnail_stats(State(_state): State) -> Result, ApiError> { +/// Get thumbnail storage statistics +#[utoipa::path( + get, + path = "/settings/thumbnail/stats", + tag = "settings", + responses( + (status = 200, body = ThumbnailStats), + (status = 401, description = "Unauthorized"), + ), + security(("Bearer" = [])) +)] +pub async fn get_thumbnail_stats(State(_state): State) -> Result, ApiError> { let settings = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'thumbnail'"#) .fetch_optional(&_state.pool) .await?;