feat(api): improve Swagger/OpenAPI documentation
- Fix Uuid and DateTime schema references (convert to String types) - Add Bearer authentication scheme with global authorize button - Add detailed descriptions to all API routes - Reorganize tags into logical categories (books, libraries, indexing, tokens) - Add security requirements and response documentation - Fix dead_code warning
This commit is contained in:
@@ -13,6 +13,7 @@ pub struct ListBooksQuery {
|
|||||||
pub library_id: Option<Uuid>,
|
pub library_id: Option<Uuid>,
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
pub kind: Option<String>,
|
pub kind: Option<String>,
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
pub cursor: Option<Uuid>,
|
pub cursor: Option<Uuid>,
|
||||||
#[schema(value_type = Option<i64>, example = 50)]
|
#[schema(value_type = Option<i64>, example = 50)]
|
||||||
pub limit: Option<i64>,
|
pub limit: Option<i64>,
|
||||||
@@ -20,7 +21,9 @@ pub struct ListBooksQuery {
|
|||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct BookItem {
|
pub struct BookItem {
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub library_id: Uuid,
|
pub library_id: Uuid,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -29,18 +32,22 @@ pub struct BookItem {
|
|||||||
pub volume: Option<String>,
|
pub volume: Option<String>,
|
||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
pub page_count: Option<i32>,
|
pub page_count: Option<i32>,
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct BooksPage {
|
pub struct BooksPage {
|
||||||
pub items: Vec<BookItem>,
|
pub items: Vec<BookItem>,
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
pub next_cursor: Option<Uuid>,
|
pub next_cursor: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct BookDetails {
|
pub struct BookDetails {
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub library_id: Uuid,
|
pub library_id: Uuid,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -54,19 +61,22 @@ pub struct BookDetails {
|
|||||||
pub file_parse_status: Option<String>,
|
pub file_parse_status: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List books with optional filtering and pagination
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/books",
|
path = "/books",
|
||||||
tag = "books",
|
tag = "books",
|
||||||
params(
|
params(
|
||||||
("library_id" = Option<Uuid>, Query, description = "Filter by library ID"),
|
("library_id" = Option<String>, Query, description = "Filter by library ID"),
|
||||||
("kind" = Option<String>, Query, description = "Filter by book kind (cbz, cbr, pdf)"),
|
("kind" = Option<String>, Query, description = "Filter by book kind (cbz, cbr, pdf)"),
|
||||||
("cursor" = Option<Uuid>, Query, description = "Cursor for pagination"),
|
("cursor" = Option<String>, Query, description = "Cursor for pagination"),
|
||||||
("limit" = Option<i64>, Query, description = "Max items to return (max 200)"),
|
("limit" = Option<i64>, Query, description = "Max items to return (max 200)"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = BooksPage),
|
(status = 200, body = BooksPage),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_books(
|
pub async fn list_books(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -120,14 +130,20 @@ pub async fn list_books(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get detailed information about a specific book
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/books/{id}",
|
path = "/books/{id}",
|
||||||
tag = "books",
|
tag = "books",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Book UUID"),
|
||||||
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = BookDetails),
|
(status = 200, body = BookDetails),
|
||||||
(status = 404, description = "Book not found"),
|
(status = 404, description = "Book not found"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn get_book(
|
pub async fn get_book(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub struct RebuildRequest {
|
|||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct IndexJobResponse {
|
pub struct IndexJobResponse {
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
pub library_id: Option<Uuid>,
|
pub library_id: Option<Uuid>,
|
||||||
@@ -26,6 +27,7 @@ pub struct IndexJobResponse {
|
|||||||
pub finished_at: Option<DateTime<Utc>>,
|
pub finished_at: Option<DateTime<Utc>>,
|
||||||
pub stats_json: Option<serde_json::Value>,
|
pub stats_json: Option<serde_json::Value>,
|
||||||
pub error_opt: Option<String>,
|
pub error_opt: Option<String>,
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +37,18 @@ pub struct FolderItem {
|
|||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enqueue a job to rebuild the search index for a library (or all libraries if no library_id specified)
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/index/rebuild",
|
path = "/index/rebuild",
|
||||||
tag = "admin",
|
tag = "indexing",
|
||||||
request_body = Option<RebuildRequest>,
|
request_body = Option<RebuildRequest>,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = IndexJobResponse),
|
(status = 200, body = IndexJobResponse),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn enqueue_rebuild(
|
pub async fn enqueue_rebuild(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -69,13 +75,17 @@ pub async fn enqueue_rebuild(
|
|||||||
Ok(Json(map_row(row)))
|
Ok(Json(map_row(row)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List recent indexing jobs with their status
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/index/status",
|
path = "/index/status",
|
||||||
tag = "admin",
|
tag = "indexing",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = Vec<IndexJobResponse>),
|
(status = 200, body = Vec<IndexJobResponse>),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_index_jobs(State(state): State<AppState>) -> Result<Json<Vec<IndexJobResponse>>, ApiError> {
|
pub async fn list_index_jobs(State(state): State<AppState>) -> Result<Json<Vec<IndexJobResponse>>, ApiError> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
@@ -87,14 +97,21 @@ pub async fn list_index_jobs(State(state): State<AppState>) -> Result<Json<Vec<I
|
|||||||
Ok(Json(rows.into_iter().map(map_row).collect()))
|
Ok(Json(rows.into_iter().map(map_row).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cancel a pending or running indexing job
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/index/cancel/{id}",
|
path = "/index/cancel/{id}",
|
||||||
tag = "admin",
|
tag = "indexing",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Job UUID"),
|
||||||
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = IndexJobResponse),
|
(status = 200, body = IndexJobResponse),
|
||||||
(status = 404, description = "Job not found or already finished"),
|
(status = 404, description = "Job not found or already finished"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn cancel_job(
|
pub async fn cancel_job(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -121,13 +138,17 @@ pub async fn cancel_job(
|
|||||||
Ok(Json(map_row(row)))
|
Ok(Json(map_row(row)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List available folders in /libraries for library creation
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/folders",
|
path = "/folders",
|
||||||
tag = "admin",
|
tag = "indexing",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = Vec<FolderItem>),
|
(status = 200, body = Vec<FolderItem>),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_folders(State(_state): State<AppState>) -> Result<Json<Vec<FolderItem>>, ApiError> {
|
pub async fn list_folders(State(_state): State<AppState>) -> Result<Json<Vec<FolderItem>>, ApiError> {
|
||||||
let libraries_path = std::path::Path::new("/libraries");
|
let libraries_path = std::path::Path::new("/libraries");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::{error::ApiError, AppState};
|
|||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct LibraryResponse {
|
pub struct LibraryResponse {
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub root_path: String,
|
pub root_path: String,
|
||||||
@@ -25,13 +26,17 @@ pub struct CreateLibraryRequest {
|
|||||||
pub root_path: String,
|
pub root_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List all libraries with their book counts
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/libraries",
|
path = "/libraries",
|
||||||
tag = "admin",
|
tag = "libraries",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = Vec<LibraryResponse>),
|
(status = 200, body = Vec<LibraryResponse>),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_libraries(State(state): State<AppState>) -> Result<Json<Vec<LibraryResponse>>, ApiError> {
|
pub async fn list_libraries(State(state): State<AppState>) -> Result<Json<Vec<LibraryResponse>>, ApiError> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
@@ -56,15 +61,19 @@ pub async fn list_libraries(State(state): State<AppState>) -> Result<Json<Vec<Li
|
|||||||
Ok(Json(items))
|
Ok(Json(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new library from an absolute path
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/libraries",
|
path = "/libraries",
|
||||||
tag = "admin",
|
tag = "libraries",
|
||||||
request_body = CreateLibraryRequest,
|
request_body = CreateLibraryRequest,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = LibraryResponse),
|
(status = 200, body = LibraryResponse),
|
||||||
(status = 400, description = "Invalid input"),
|
(status = 400, description = "Invalid input"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn create_library(
|
pub async fn create_library(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -96,14 +105,21 @@ pub async fn create_library(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a library by ID
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/libraries/{id}",
|
path = "/libraries/{id}",
|
||||||
tag = "admin",
|
tag = "libraries",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Library UUID"),
|
||||||
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Library deleted"),
|
(status = 200, description = "Library deleted"),
|
||||||
(status = 404, description = "Library not found"),
|
(status = 404, description = "Library not found"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn delete_library(
|
pub async fn delete_library(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
@@ -35,11 +36,73 @@ use utoipa::OpenApi;
|
|||||||
crate::tokens::CreateTokenRequest,
|
crate::tokens::CreateTokenRequest,
|
||||||
crate::tokens::TokenResponse,
|
crate::tokens::TokenResponse,
|
||||||
crate::tokens::CreatedTokenResponse,
|
crate::tokens::CreatedTokenResponse,
|
||||||
|
ErrorResponse,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
tags(
|
tags(
|
||||||
(name = "books", description = "Books management endpoints"),
|
(name = "books", description = "Read-only endpoints for browsing and searching books"),
|
||||||
(name = "admin", description = "Admin management endpoints"),
|
(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)"),
|
||||||
|
),
|
||||||
|
modifiers(&SecurityAddon)
|
||||||
)]
|
)]
|
||||||
pub struct ApiDoc;
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
pub struct SecurityAddon;
|
||||||
|
|
||||||
|
impl utoipa::Modify for SecurityAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
|
components.add_security_scheme(
|
||||||
|
"Bearer",
|
||||||
|
SecurityScheme::Http(
|
||||||
|
HttpBuilder::new()
|
||||||
|
.scheme(HttpAuthScheme::Bearer)
|
||||||
|
.bearer_format("JWT")
|
||||||
|
.description(Some(
|
||||||
|
"Enter your API Bearer token (format: stl_<prefix>_<secret>)",
|
||||||
|
))
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(utoipa::ToSchema)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_openapi_generation() {
|
||||||
|
let api_doc = ApiDoc::openapi();
|
||||||
|
let json = api_doc
|
||||||
|
.to_pretty_json()
|
||||||
|
.expect("Failed to serialize OpenAPI");
|
||||||
|
|
||||||
|
// Check that there are no references to non-existent schemas
|
||||||
|
assert!(
|
||||||
|
!json.contains("\"/components/schemas/Uuid\""),
|
||||||
|
"Uuid schema should not be referenced"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!json.contains("\"/components/schemas/DateTime\""),
|
||||||
|
"DateTime schema should not be referenced"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save to file for inspection
|
||||||
|
std::fs::write("/tmp/openapi.json", &json).expect("Failed to write file");
|
||||||
|
println!("OpenAPI JSON saved to /tmp/openapi.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,13 +64,14 @@ impl OutputFormat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a specific page image from a book with optional format conversion
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/books/{book_id}/pages/{n}",
|
path = "/books/{book_id}/pages/{n}",
|
||||||
tag = "books",
|
tag = "books",
|
||||||
params(
|
params(
|
||||||
("book_id" = Uuid, description = "Book UUID"),
|
("book_id" = String, Path, description = "Book UUID"),
|
||||||
("n" = u32, description = "Page number (starts at 1)"),
|
("n" = u32, Path, description = "Page number (starts at 1)"),
|
||||||
("format" = Option<String>, Query, description = "Output format: webp, jpeg, png"),
|
("format" = Option<String>, Query, description = "Output format: webp, jpeg, png"),
|
||||||
("quality" = Option<u8>, Query, description = "JPEG quality 1-100"),
|
("quality" = Option<u8>, Query, description = "JPEG quality 1-100"),
|
||||||
("width" = Option<u32>, Query, description = "Max width (max 2160)"),
|
("width" = Option<u32>, Query, description = "Max width (max 2160)"),
|
||||||
@@ -79,7 +80,9 @@ impl OutputFormat {
|
|||||||
(status = 200, description = "Page image", content_type = "image/webp"),
|
(status = 200, description = "Page image", content_type = "image/webp"),
|
||||||
(status = 400, description = "Invalid parameters"),
|
(status = 400, description = "Invalid parameters"),
|
||||||
(status = 404, description = "Book or page not found"),
|
(status = 404, description = "Book or page not found"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn get_page(
|
pub async fn get_page(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ pub struct SearchResponse {
|
|||||||
pub processing_time_ms: Option<u64>,
|
pub processing_time_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search books across all libraries using Meilisearch
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/search",
|
path = "/search",
|
||||||
tag = "books",
|
tag = "books",
|
||||||
params(
|
params(
|
||||||
("q" = String, description = "Search query"),
|
("q" = String, Query, description = "Search query"),
|
||||||
("library_id" = Option<String>, Query, description = "Filter by library ID"),
|
("library_id" = Option<String>, Query, description = "Filter by library ID"),
|
||||||
("type" = Option<String>, Query, description = "Filter by type (cbz, cbr, pdf)"),
|
("type" = Option<String>, Query, description = "Filter by type (cbz, cbr, pdf)"),
|
||||||
("kind" = Option<String>, Query, description = "Filter by kind (alias for type)"),
|
("kind" = Option<String>, Query, description = "Filter by kind (alias for type)"),
|
||||||
@@ -38,7 +39,9 @@ pub struct SearchResponse {
|
|||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = SearchResponse),
|
(status = 200, body = SearchResponse),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn search_books(
|
pub async fn search_books(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct CreateTokenRequest {
|
|||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct TokenResponse {
|
pub struct TokenResponse {
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub scope: String,
|
pub scope: String,
|
||||||
@@ -28,11 +29,13 @@ pub struct TokenResponse {
|
|||||||
pub last_used_at: Option<DateTime<Utc>>,
|
pub last_used_at: Option<DateTime<Utc>>,
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
pub revoked_at: Option<DateTime<Utc>>,
|
pub revoked_at: Option<DateTime<Utc>>,
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct CreatedTokenResponse {
|
pub struct CreatedTokenResponse {
|
||||||
|
#[schema(value_type = String)]
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub scope: String,
|
pub scope: String,
|
||||||
@@ -40,15 +43,19 @@ pub struct CreatedTokenResponse {
|
|||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new API token with read or admin scope. The token is only shown once.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/admin/tokens",
|
path = "/admin/tokens",
|
||||||
tag = "admin",
|
tag = "tokens",
|
||||||
request_body = CreateTokenRequest,
|
request_body = CreateTokenRequest,
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = CreatedTokenResponse, description = "Token created - token is only shown once"),
|
(status = 200, body = CreatedTokenResponse, description = "Token created - token is only shown once"),
|
||||||
(status = 400, description = "Invalid input"),
|
(status = 400, description = "Invalid input"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn create_token(
|
pub async fn create_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -97,13 +104,17 @@ pub async fn create_token(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List all API tokens
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/admin/tokens",
|
path = "/admin/tokens",
|
||||||
tag = "admin",
|
tag = "tokens",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = Vec<TokenResponse>),
|
(status = 200, body = Vec<TokenResponse>),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_tokens(State(state): State<AppState>) -> Result<Json<Vec<TokenResponse>>, ApiError> {
|
pub async fn list_tokens(State(state): State<AppState>) -> Result<Json<Vec<TokenResponse>>, ApiError> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
@@ -128,14 +139,21 @@ pub async fn list_tokens(State(state): State<AppState>) -> Result<Json<Vec<Token
|
|||||||
Ok(Json(items))
|
Ok(Json(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Revoke an API token by ID
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/admin/tokens/{id}",
|
path = "/admin/tokens/{id}",
|
||||||
tag = "admin",
|
tag = "tokens",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Token UUID"),
|
||||||
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Token revoked"),
|
(status = 200, description = "Token revoked"),
|
||||||
(status = 404, description = "Token not found"),
|
(status = 404, description = "Token not found"),
|
||||||
)
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 403, description = "Forbidden - Admin scope required"),
|
||||||
|
),
|
||||||
|
security(("Bearer" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn revoke_token(
|
pub async fn revoke_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|||||||
Reference in New Issue
Block a user