docs(api): complete OpenAPI coverage for all routes

Add missing utoipa annotations:
- GET /books/{id}/thumbnail
- GET/POST /settings, /settings/{key}
- POST /settings/cache/clear
- GET /settings/cache/stats, /settings/thumbnail/stats
Add 'settings' tag and register all new schemas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 22:23:28 +01:00
parent 473e849dfa
commit f1b3aec94a
3 changed files with 108 additions and 10 deletions

View File

@@ -347,6 +347,21 @@ use axum::{
response::IntoResponse, 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( pub async fn get_thumbnail(
State(state): State<AppState>, State(state): State<AppState>,
Path(book_id): Path<Uuid>, Path(book_id): Path<Uuid>,

View File

@@ -6,6 +6,7 @@ use utoipa::OpenApi;
paths( paths(
crate::books::list_books, crate::books::list_books,
crate::books::get_book, crate::books::get_book,
crate::books::get_thumbnail,
crate::books::list_series, crate::books::list_series,
crate::pages::get_page, crate::pages::get_page,
crate::search::search_books, crate::search::search_books,
@@ -27,6 +28,12 @@ use utoipa::OpenApi;
crate::tokens::list_tokens, crate::tokens::list_tokens,
crate::tokens::create_token, crate::tokens::create_token,
crate::tokens::revoke_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( components(
schemas( schemas(
@@ -51,6 +58,10 @@ use utoipa::OpenApi;
crate::tokens::CreateTokenRequest, crate::tokens::CreateTokenRequest,
crate::tokens::TokenResponse, crate::tokens::TokenResponse,
crate::tokens::CreatedTokenResponse, crate::tokens::CreatedTokenResponse,
crate::settings::UpdateSettingRequest,
crate::settings::ClearCacheResponse,
crate::settings::CacheStats,
crate::settings::ThumbnailStats,
ErrorResponse, ErrorResponse,
) )
), ),
@@ -62,6 +73,7 @@ use utoipa::OpenApi;
(name = "libraries", description = "Library management endpoints (Admin only)"), (name = "libraries", description = "Library management endpoints (Admin only)"),
(name = "indexing", description = "Search index management and job control (Admin only)"), (name = "indexing", description = "Search index management and job control (Admin only)"),
(name = "tokens", description = "API token management (Admin only)"), (name = "tokens", description = "API token management (Admin only)"),
(name = "settings", description = "Application settings and cache management (Admin only)"),
), ),
modifiers(&SecurityAddon) modifiers(&SecurityAddon)
)] )]

View File

@@ -6,28 +6,29 @@ use axum::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use sqlx::Row; use sqlx::Row;
use utoipa::ToSchema;
use crate::{error::ApiError, state::AppState}; use crate::{error::ApiError, state::AppState};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UpdateSettingRequest { pub struct UpdateSettingRequest {
pub value: Value, pub value: Value,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ClearCacheResponse { pub struct ClearCacheResponse {
pub success: bool, pub success: bool,
pub message: String, pub message: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CacheStats { pub struct CacheStats {
pub total_size_mb: f64, pub total_size_mb: f64,
pub file_count: u64, pub file_count: u64,
pub directory: String, pub directory: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ThumbnailStats { pub struct ThumbnailStats {
pub total_size_mb: f64, pub total_size_mb: f64,
pub file_count: u64, pub file_count: u64,
@@ -43,7 +44,18 @@ pub fn settings_routes() -> Router<AppState> {
.route("/settings/thumbnail/stats", get(get_thumbnail_stats)) .route("/settings/thumbnail/stats", get(get_thumbnail_stats))
} }
async fn get_settings(State(state): State<AppState>) -> Result<Json<Value>, 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<AppState>) -> Result<Json<Value>, ApiError> {
let rows = sqlx::query(r#"SELECT key, value FROM app_settings"#) let rows = sqlx::query(r#"SELECT key, value FROM app_settings"#)
.fetch_all(&state.pool) .fetch_all(&state.pool)
.await?; .await?;
@@ -58,7 +70,20 @@ async fn get_settings(State(state): State<AppState>) -> Result<Json<Value>, ApiE
Ok(Json(Value::Object(settings))) 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<AppState>, State(state): State<AppState>,
axum::extract::Path(key): axum::extract::Path<String>, axum::extract::Path(key): axum::extract::Path<String>,
) -> Result<Json<Value>, ApiError> { ) -> Result<Json<Value>, 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<AppState>, State(state): State<AppState>,
axum::extract::Path(key): axum::extract::Path<String>, axum::extract::Path(key): axum::extract::Path<String>,
Json(body): Json<UpdateSettingRequest>, Json(body): Json<UpdateSettingRequest>,
@@ -99,7 +137,18 @@ async fn update_setting(
Ok(Json(value)) Ok(Json(value))
} }
async fn clear_cache(State(_state): State<AppState>) -> Result<Json<ClearCacheResponse>, 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<AppState>) -> Result<Json<ClearCacheResponse>, ApiError> {
let cache_dir = std::env::var("IMAGE_CACHE_DIR") let cache_dir = std::env::var("IMAGE_CACHE_DIR")
.unwrap_or_else(|_| "/tmp/stripstream-image-cache".to_string()); .unwrap_or_else(|_| "/tmp/stripstream-image-cache".to_string());
@@ -128,7 +177,18 @@ async fn clear_cache(State(_state): State<AppState>) -> Result<Json<ClearCacheRe
Ok(Json(result)) Ok(Json(result))
} }
async fn get_cache_stats(State(_state): State<AppState>) -> Result<Json<CacheStats>, 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<AppState>) -> Result<Json<CacheStats>, ApiError> {
let cache_dir = std::env::var("IMAGE_CACHE_DIR") let cache_dir = std::env::var("IMAGE_CACHE_DIR")
.unwrap_or_else(|_| "/tmp/stripstream-image-cache".to_string()); .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) (total_size, file_count)
} }
async fn get_thumbnail_stats(State(_state): State<AppState>) -> Result<Json<ThumbnailStats>, 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<AppState>) -> Result<Json<ThumbnailStats>, ApiError> {
let settings = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'thumbnail'"#) let settings = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'thumbnail'"#)
.fetch_optional(&_state.pool) .fetch_optional(&_state.pool)
.await?; .await?;