This commit is contained in:
2025-10-25 23:46:01 +02:00
parent d7aefc4110
commit 62cadbcfa0
8 changed files with 1061 additions and 55 deletions
+8
View File
@@ -0,0 +1,8 @@
use backend::openapi::ApiDoc;
use utoipa::OpenApi;
fn main() {
let spec = ApiDoc::openapi();
let json = serde_json::to_string_pretty(&spec).expect("serialize openapi");
println!("{}", json);
}
+1
View File
@@ -4,6 +4,7 @@ pub mod db;
pub mod error;
pub mod jobs;
pub mod models;
pub mod openapi;
pub mod routes;
pub mod s3;
pub mod schema;
+902
View File
@@ -0,0 +1,902 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use utoipa::{IntoParams, OpenApi, ToSchema};
use uuid::Uuid;
#[derive(OpenApi)]
#[openapi(
paths(
doc::health_check,
doc::login,
doc::refresh,
doc::logout,
doc::me,
doc::select_tenant,
doc::list_documents,
doc::upload_document,
doc::get_document,
doc::update_document,
doc::delete_document,
doc::download_document,
doc::download_with_token,
doc::move_document,
doc::assign_tags,
doc::remove_tag,
doc::bulk_move_documents,
doc::bulk_update_tags,
doc::bulk_assign_correspondents,
doc::assign_correspondents,
doc::remove_correspondent,
doc::reanalyze_selected_documents,
doc::list_document_assets,
doc::request_document_assets,
doc::get_document_asset,
doc::create_folder,
doc::ensure_folder_path,
doc::get_folder,
doc::list_folder_contents,
doc::delete_folder,
doc::update_folder,
doc::list_tags,
doc::create_tag,
doc::update_tag,
doc::delete_tag,
doc::list_correspondents,
doc::create_correspondent,
doc::update_correspondent,
doc::delete_correspondent,
),
components(
schemas(
schemas::LoginRequest,
schemas::AccessTokenResponse,
schemas::TenantSummary,
schemas::TenantSelectionResponse,
schemas::TenantSelectionRequest,
schemas::LoginResponseVariants,
schemas::DocumentResponse,
schemas::DocumentDetailResponse,
schemas::DocumentVersion,
schemas::DocumentAssetSummary,
schemas::DocumentAssetDetail,
schemas::DocumentAssetObject,
schemas::DocumentCorrespondent,
schemas::DocumentTag,
schemas::DocumentDownloadResponse,
schemas::UpdateDocumentRequest,
schemas::BulkMoveDocumentsRequest,
schemas::BulkMoveDocumentsResponse,
schemas::AssignTagsRequest,
schemas::MoveDocumentRequest,
schemas::BulkTagRequest,
schemas::BulkTagResponse,
schemas::CorrespondentAssignment,
schemas::BulkTagAction,
schemas::BulkCorrespondentsRequest,
schemas::BulkCorrespondentsResponse,
schemas::BulkCorrespondentAction,
schemas::AssignCorrespondentsRequest,
schemas::RemoveCorrespondentParams,
schemas::ReanalyzeRequest,
schemas::ReanalyzeResponse,
schemas::DocumentAssetRequestParams,
schemas::AssetObjectsQuery,
schemas::UploadDocumentForm,
schemas::CreateFolderRequest,
schemas::EnsureFolderPathRequest,
schemas::FolderResponse,
schemas::FolderInfo,
schemas::FolderContentsResponse,
schemas::FolderDocumentSummary,
schemas::UpdateFolderRequest,
schemas::FolderContentsParams,
schemas::TagCatalogEntry,
schemas::CreateTagRequest,
schemas::UpdateTagRequest,
schemas::CorrespondentCatalogEntry,
schemas::CreateCorrespondentRequest,
schemas::UpdateCorrespondentRequest,
)
),
tags(
(name = "Health", description = "Service health"),
(name = "Auth", description = "Authentication"),
(name = "Documents", description = "Document management"),
(name = "Assets", description = "Document assets"),
(name = "Folders", description = "Folder management"),
(name = "Tags", description = "Tag catalog"),
(name = "Correspondents", description = "Correspondent catalog")
)
)]
pub struct ApiDoc;
#[allow(dead_code)]
mod doc {
use super::schemas::*;
use uuid::Uuid;
#[allow(dead_code)]
fn __keep_uuid_import() {
let _ = Uuid::nil();
}
#[utoipa::path(
get,
path = "/api/health",
responses((status = 200, description = "Service is healthy")),
tag = "Health"
)]
pub(super) fn health_check() {}
#[utoipa::path(
post,
path = "/api/auth/login",
request_body = LoginRequest,
responses(
(status = 200, description = "Login succeeded", body = LoginResponseVariants),
(status = 401, description = "Invalid credentials")
),
tag = "Auth"
)]
pub(super) fn login() {}
#[utoipa::path(
post,
path = "/api/auth/refresh",
responses(
(status = 200, description = "Refreshed access token", body = AccessTokenResponse),
(status = 401, description = "Missing or invalid refresh token")
),
tag = "Auth"
)]
pub(super) fn refresh() {}
#[utoipa::path(
post,
path = "/api/auth/logout",
responses((status = 204, description = "Session revoked")),
tag = "Auth"
)]
pub(super) fn logout() {}
#[utoipa::path(
get,
path = "/api/auth/me",
responses((status = 200, description = "Authenticated principal", body = AccessTokenResponse)),
tag = "Auth"
)]
pub(super) fn me() {}
#[utoipa::path(
post,
path = "/api/auth/select-tenant",
request_body = TenantSelectionRequest,
responses((status = 200, description = "Tenant selected", body = AccessTokenResponse)),
tag = "Auth"
)]
pub(super) fn select_tenant() {}
#[utoipa::path(
get,
path = "/api/documents",
params(DocumentListQuery),
responses((status = 200, description = "List documents", body = [DocumentResponse])),
tag = "Documents"
)]
pub(super) fn list_documents() {}
#[utoipa::path(
post,
path = "/api/documents",
request_body = UploadDocumentForm,
responses((status = 201, description = "Document uploaded", body = DocumentDetailResponse)),
tag = "Documents"
)]
pub(super) fn upload_document() {}
#[utoipa::path(
get,
path = "/api/documents/{id}",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Document detail", body = DocumentDetailResponse)),
tag = "Documents"
)]
pub(super) fn get_document() {}
#[utoipa::path(
patch,
path = "/api/documents/{id}",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = UpdateDocumentRequest,
responses((status = 200, description = "Updated document", body = DocumentDetailResponse)),
tag = "Documents"
)]
pub(super) fn update_document() {}
#[utoipa::path(
delete,
path = "/api/documents/{id}",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 204, description = "Document deleted")),
tag = "Documents"
)]
pub(super) fn delete_document() {}
#[utoipa::path(
get,
path = "/api/documents/{id}/download",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Download metadata", body = DocumentDownloadResponse)),
tag = "Documents"
)]
pub(super) fn download_document() {}
#[utoipa::path(
get,
path = "/download/{token}",
params(("token" = String, Path, description = "Download token")),
responses((status = 302, description = "Redirect to pre-signed URL")),
tag = "Documents"
)]
pub(super) fn download_with_token() {}
#[utoipa::path(
patch,
path = "/api/documents/{id}/folder",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = MoveDocumentRequest,
responses((status = 204, description = "Document moved")),
tag = "Documents"
)]
pub(super) fn move_document() {}
#[utoipa::path(
post,
path = "/api/documents/{id}/tags",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = AssignTagsRequest,
responses((status = 204, description = "Tags assigned")),
tag = "Documents"
)]
pub(super) fn assign_tags() {}
#[utoipa::path(
delete,
path = "/api/documents/{id}/tags/{tag_id}",
params(
("id" = Uuid, Path, description = "Document ID"),
("tag_id" = Uuid, Path, description = "Tag ID")
),
responses((status = 204, description = "Tag removed")),
tag = "Documents"
)]
pub(super) fn remove_tag() {}
#[utoipa::path(
post,
path = "/api/documents/bulk/move",
request_body = BulkMoveDocumentsRequest,
responses((status = 200, description = "Bulk move outcome", body = BulkMoveDocumentsResponse)),
tag = "Documents"
)]
pub(super) fn bulk_move_documents() {}
#[utoipa::path(
post,
path = "/api/documents/bulk/tags",
request_body = BulkTagRequest,
responses((status = 200, description = "Bulk tag outcome", body = BulkTagResponse)),
tag = "Documents"
)]
pub(super) fn bulk_update_tags() {}
#[utoipa::path(
post,
path = "/api/documents/bulk/correspondents",
request_body = BulkCorrespondentsRequest,
responses((status = 200, description = "Bulk correspondents outcome", body = BulkCorrespondentsResponse)),
tag = "Documents"
)]
pub(super) fn bulk_assign_correspondents() {}
#[utoipa::path(
post,
path = "/api/documents/{id}/correspondents",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = AssignCorrespondentsRequest,
responses((status = 204, description = "Correspondents assigned")),
tag = "Documents"
)]
pub(super) fn assign_correspondents() {}
#[utoipa::path(
delete,
path = "/api/documents/{id}/correspondents/{correspondent_id}",
params(
("id" = Uuid, Path, description = "Document ID"),
("correspondent_id" = Uuid, Path, description = "Correspondent ID"),
RemoveCorrespondentParams
),
responses((status = 204, description = "Correspondent removed")),
tag = "Documents"
)]
pub(super) fn remove_correspondent() {}
#[utoipa::path(
post,
path = "/api/documents/bulk/reanalyze",
request_body = ReanalyzeRequest,
responses((status = 200, description = "Reanalyze queued", body = ReanalyzeResponse)),
tag = "Documents"
)]
pub(super) fn reanalyze_selected_documents() {}
#[utoipa::path(
get,
path = "/api/documents/{id}/assets",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Document assets", body = [DocumentAssetSummary])),
tag = "Assets"
)]
pub(super) fn list_document_assets() {}
#[utoipa::path(
post,
path = "/api/documents/{id}/assets",
params(
("id" = Uuid, Path, description = "Document ID"),
DocumentAssetRequestParams
),
responses((status = 202, description = "Asset generation requested")),
tag = "Assets"
)]
pub(super) fn request_document_assets() {}
#[utoipa::path(
get,
path = "/api/assets/{asset_id}",
params(
("asset_id" = Uuid, Path, description = "Asset ID"),
AssetObjectsQuery
),
responses((status = 200, description = "Asset detail", body = DocumentAssetDetail)),
tag = "Assets"
)]
pub(super) fn get_document_asset() {}
#[utoipa::path(
post,
path = "/api/folders",
request_body = CreateFolderRequest,
responses((status = 200, description = "Folder created", body = FolderResponse)),
tag = "Folders"
)]
pub(super) fn create_folder() {}
#[utoipa::path(
post,
path = "/api/folders/path",
request_body = EnsureFolderPathRequest,
responses((status = 200, description = "Folder path ensured", body = FolderResponse)),
tag = "Folders"
)]
pub(super) fn ensure_folder_path() {}
#[utoipa::path(
get,
path = "/api/folders/{id}",
params(("id" = Uuid, Path, description = "Folder ID")),
responses((status = 200, description = "Folder detail", body = FolderResponse)),
tag = "Folders"
)]
pub(super) fn get_folder() {}
#[utoipa::path(
get,
path = "/api/folders/{id}/contents",
params(
("id" = Uuid, Path, description = "Folder ID"),
FolderContentsParams
),
responses((status = 200, description = "Folder contents", body = FolderContentsResponse)),
tag = "Folders"
)]
pub(super) fn list_folder_contents() {}
#[utoipa::path(
delete,
path = "/api/folders/{id}",
params(("id" = Uuid, Path, description = "Folder ID")),
responses((status = 204, description = "Folder deleted")),
tag = "Folders"
)]
pub(super) fn delete_folder() {}
#[utoipa::path(
patch,
path = "/api/folders/{id}",
params(("id" = Uuid, Path, description = "Folder ID")),
request_body = UpdateFolderRequest,
responses((status = 204, description = "Folder updated")),
tag = "Folders"
)]
pub(super) fn update_folder() {}
#[utoipa::path(
get,
path = "/api/tags",
responses((status = 200, description = "Tags", body = [TagCatalogEntry])),
tag = "Tags"
)]
pub(super) fn list_tags() {}
#[utoipa::path(
post,
path = "/api/tags",
request_body = CreateTagRequest,
responses((status = 200, description = "Tag created", body = TagCatalogEntry)),
tag = "Tags"
)]
pub(super) fn create_tag() {}
#[utoipa::path(
patch,
path = "/api/tags/{id}",
params(("id" = Uuid, Path, description = "Tag ID")),
request_body = UpdateTagRequest,
responses((status = 200, description = "Tag updated", body = TagCatalogEntry)),
tag = "Tags"
)]
pub(super) fn update_tag() {}
#[utoipa::path(
delete,
path = "/api/tags/{id}",
params(("id" = Uuid, Path, description = "Tag ID")),
responses((status = 204, description = "Tag deleted")),
tag = "Tags"
)]
pub(super) fn delete_tag() {}
#[utoipa::path(
get,
path = "/api/correspondents",
responses((status = 200, description = "Correspondents", body = [CorrespondentCatalogEntry])),
tag = "Correspondents"
)]
pub(super) fn list_correspondents() {}
#[utoipa::path(
post,
path = "/api/correspondents",
request_body = CreateCorrespondentRequest,
responses((status = 200, description = "Correspondent created", body = CorrespondentCatalogEntry)),
tag = "Correspondents"
)]
pub(super) fn create_correspondent() {}
#[utoipa::path(
patch,
path = "/api/correspondents/{id}",
params(("id" = Uuid, Path, description = "Correspondent ID")),
request_body = UpdateCorrespondentRequest,
responses((status = 200, description = "Correspondent updated", body = CorrespondentCatalogEntry)),
tag = "Correspondents"
)]
pub(super) fn update_correspondent() {}
#[utoipa::path(
delete,
path = "/api/correspondents/{id}",
params(("id" = Uuid, Path, description = "Correspondent ID")),
responses((status = 204, description = "Correspondent deleted")),
tag = "Correspondents"
)]
pub(super) fn delete_correspondent() {}
}
#[cfg(test)]
mod tests {
use super::ApiDoc;
use utoipa::OpenApi;
#[test]
fn openapi_serializes() {
let spec = ApiDoc::openapi();
let _ = serde_json::to_string(&spec).expect("serialize openapi");
}
}
pub mod schemas {
use super::*;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct LoginRequest {
pub username: String,
pub password: String,
#[schema(nullable)]
pub preferred_tenant_slug: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct AccessTokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: i64,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantSummary {
pub tenant_id: Uuid,
pub slug: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantSelectionResponse {
pub selection_token: String,
pub tenants: Vec<TenantSummary>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantSelectionRequest {
pub tenant_id: Uuid,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum LoginResponseVariants {
Token(AccessTokenResponse),
Selection(TenantSelectionResponse),
}
#[derive(Serialize, Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct DocumentListQuery {
pub folder_id: Option<Uuid>,
pub include_deleted: Option<bool>,
pub include_descendants: Option<bool>,
pub query: Option<String>,
pub tags: Option<String>,
pub correspondents: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentTag {
pub id: Uuid,
pub label: String,
#[schema(nullable)]
pub color: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentAssetObject {
pub id: Uuid,
pub ordinal: i32,
pub metadata: Value,
#[schema(nullable)]
pub url: Option<String>,
#[schema(nullable)]
pub expires_at: Option<i64>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentAssetSummary {
pub id: Uuid,
pub asset_type: String,
pub mime_type: String,
pub metadata: Value,
#[schema(nullable)]
pub cardinality: Option<i32>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentAssetDetail {
pub id: Uuid,
pub asset_type: String,
pub mime_type: String,
pub metadata: Value,
pub created_at: String,
#[schema(nullable)]
pub cardinality: Option<i32>,
pub objects: Vec<DocumentAssetObject>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentVersion {
pub id: Uuid,
pub version_number: i32,
pub checksum: String,
pub size_bytes: i64,
pub created_at: String,
pub metadata: Value,
#[schema(nullable)]
pub operations_summary: Option<Value>,
#[schema(nullable)]
pub assets: Option<Vec<DocumentAssetSummary>>,
pub download_path: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentCorrespondent {
pub id: Uuid,
pub name: String,
pub role: String,
pub metadata: Value,
pub assigned_at: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentResponse {
pub id: Uuid,
pub filename: String,
pub title: String,
pub original_name: String,
#[schema(nullable)]
pub content_type: Option<String>,
#[schema(nullable)]
pub folder_id: Option<Uuid>,
pub uploaded_at: String,
pub updated_at: String,
#[schema(nullable)]
pub deleted_at: Option<String>,
#[schema(nullable)]
pub issued_at: Option<String>,
pub metadata: Value,
pub tags: Vec<DocumentTag>,
#[schema(nullable)]
pub correspondents: Option<Vec<DocumentCorrespondent>>,
#[schema(nullable)]
pub current_version: Option<DocumentVersion>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentDetailResponse {
pub document: DocumentResponse,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct DocumentDownloadResponse {
pub url: String,
pub expires_in: u64,
pub filename: String,
#[schema(nullable)]
pub content_type: Option<String>,
pub size_bytes: i64,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UpdateDocumentRequest {
#[schema(nullable)]
pub title: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BulkMoveDocumentsRequest {
pub document_ids: Vec<Uuid>,
#[schema(nullable)]
pub folder_id: Option<Uuid>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BulkMoveDocumentsResponse {
pub updated: usize,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct AssignTagsRequest {
pub tag_ids: Vec<Uuid>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct MoveDocumentRequest {
#[schema(nullable)]
pub folder_id: Option<Uuid>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BulkTagAction {
Add,
Remove,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BulkTagRequest {
pub document_ids: Vec<Uuid>,
pub tag_ids: Vec<Uuid>,
pub action: BulkTagAction,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BulkTagResponse {
pub added: usize,
pub removed: usize,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct CorrespondentAssignment {
pub correspondent_id: Uuid,
pub role: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct AssignCorrespondentsRequest {
pub assignments: Vec<CorrespondentAssignment>,
#[serde(default)]
pub replace: bool,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum BulkCorrespondentAction {
Add,
Remove,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BulkCorrespondentsRequest {
pub document_ids: Vec<Uuid>,
pub assignments: Vec<CorrespondentAssignment>,
#[serde(default = "default_bulk_correspondent_action")]
pub action: BulkCorrespondentAction,
}
fn default_bulk_correspondent_action() -> BulkCorrespondentAction {
BulkCorrespondentAction::Add
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BulkCorrespondentsResponse {
pub assigned: usize,
pub removed: usize,
}
#[derive(Serialize, Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct RemoveCorrespondentParams {
pub role: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ReanalyzeRequest {
pub document_ids: Vec<Uuid>,
#[serde(default)]
pub force: bool,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ReanalyzeResponse {
pub queued: usize,
}
#[derive(Serialize, Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct DocumentAssetRequestParams {
#[serde(default)]
pub force: Option<bool>,
}
#[derive(Serialize, Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct AssetObjectsQuery {
#[serde(default)]
pub start: Option<i32>,
#[serde(default)]
pub limit: Option<i32>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadDocumentForm {
#[schema(value_type = String, format = Binary)]
pub file: String,
#[schema(nullable)]
pub folder_id: Option<Uuid>,
#[schema(nullable)]
pub metadata: Option<Value>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct CreateFolderRequest {
pub name: String,
#[schema(nullable)]
pub parent_id: Option<Uuid>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EnsureFolderPathRequest {
#[schema(nullable)]
pub parent_id: Option<Uuid>,
pub segments: Vec<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct FolderInfo {
pub id: Uuid,
pub name: String,
#[schema(nullable)]
pub parent_id: Option<Uuid>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct FolderResponse {
pub folder: FolderInfo,
}
#[derive(Serialize, Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct FolderContentsParams {
#[serde(default)]
pub include_documents: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct FolderDocumentSummary {
pub id: Uuid,
pub title: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct FolderContentsResponse {
#[schema(nullable)]
pub folder: Option<FolderInfo>,
pub subfolders: Vec<FolderInfo>,
pub documents: Vec<FolderDocumentSummary>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UpdateFolderRequest {
#[schema(nullable)]
pub parent_id: Option<Option<Uuid>>,
#[schema(nullable)]
pub name: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct TagCatalogEntry {
pub id: Uuid,
pub label: String,
#[schema(nullable)]
pub color: Option<String>,
pub usage_count: i64,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct CreateTagRequest {
pub label: String,
#[schema(nullable)]
pub color: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UpdateTagRequest {
#[schema(nullable)]
pub label: Option<Option<String>>,
#[schema(nullable)]
pub color: Option<Option<String>>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct CorrespondentCatalogEntry {
pub id: Uuid,
pub name: String,
pub metadata: Value,
pub role_counts: Value,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct CreateCorrespondentRequest {
pub name: String,
#[schema(default, value_type = Object)]
pub metadata: Value,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UpdateCorrespondentRequest {
#[schema(nullable)]
pub name: Option<String>,
#[schema(nullable)]
pub metadata: Option<Value>,
}
}
+44 -29
View File
@@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use tracing::{debug, error, info, warn};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use super::folders::gather_descendant_folder_ids;
@@ -55,7 +56,8 @@ use search_utils::{build_quickwit_query, extract_document_id};
const PRESIGNED_URL_EXPIRY_SECONDS: u64 = 300;
const QUICKWIT_MAX_HITS: usize = 200;
#[derive(Deserialize)]
#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct DocumentListQuery {
pub folder_id: Option<Uuid>,
#[serde(default)]
@@ -67,13 +69,14 @@ pub struct DocumentListQuery {
pub correspondents: Option<String>,
}
#[derive(Deserialize)]
#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct AssetRequestQuery {
#[serde(default)]
pub force: bool,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct TagResponse {
pub id: Uuid,
pub label: String,
@@ -90,7 +93,7 @@ impl From<Tag> for TagResponse {
}
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentVersionResponse {
pub id: Uuid,
pub version_number: i32,
@@ -103,7 +106,7 @@ pub struct DocumentVersionResponse {
pub operations_summary: Option<Value>,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentAssetResponse {
pub id: Uuid,
pub asset_type: String,
@@ -113,7 +116,7 @@ pub struct DocumentAssetResponse {
pub cardinality: Option<i32>,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentAssetObjectResponse {
pub id: Uuid,
pub ordinal: i32,
@@ -124,7 +127,7 @@ pub struct DocumentAssetObjectResponse {
pub expires_at: Option<i64>,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct DocumentAssetDetailResponse {
pub id: Uuid,
pub asset_type: String,
@@ -137,7 +140,7 @@ pub struct DocumentAssetDetailResponse {
pub objects: Vec<DocumentAssetObjectResponse>,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentCurrentVersionResponse {
#[serde(flatten)]
pub version: DocumentVersionResponse,
@@ -146,7 +149,7 @@ pub struct DocumentCurrentVersionResponse {
pub download_path: String,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentCorrespondentResponse {
pub id: Uuid,
pub name: String,
@@ -155,7 +158,7 @@ pub struct DocumentCorrespondentResponse {
pub assigned_at: String,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct DocumentResponse {
pub id: Uuid,
pub filename: String,
@@ -174,12 +177,12 @@ pub struct DocumentResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub current_version: Option<DocumentCurrentVersionResponse>,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct DocumentDetailResponse {
pub document: DocumentResponse,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct DocumentDownloadResponse {
pub url: String,
pub expires_in: u64,
@@ -188,67 +191,67 @@ pub struct DocumentDownloadResponse {
pub size_bytes: i64,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct BulkReanalyzeResponse {
pub queued: usize,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct BulkMoveRequest {
pub document_ids: Vec<Uuid>,
pub folder_id: Option<Uuid>,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct UpdateDocumentRequest {
pub title: Option<String>,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct BulkMoveResponse {
pub updated: usize,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BulkTagAction {
Add,
Remove,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct BulkTagRequest {
pub document_ids: Vec<Uuid>,
pub tag_ids: Vec<Uuid>,
pub action: BulkTagAction,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct BulkTagResponse {
pub added: usize,
pub removed: usize,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub struct BulkCorrespondentResponse {
pub assigned: usize,
pub removed: usize,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct CorrespondentAssignmentInput {
pub correspondent_id: Uuid,
pub role: String,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct AssignCorrespondentsRequest {
pub assignments: Vec<CorrespondentAssignmentInput>,
#[serde(default)]
pub replace: bool,
}
#[derive(Deserialize, Copy, Clone, PartialEq, Eq)]
#[derive(Deserialize, Copy, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum BulkCorrespondentAction {
Add,
@@ -259,7 +262,7 @@ fn default_bulk_correspondent_action() -> BulkCorrespondentAction {
BulkCorrespondentAction::Add
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct BulkCorrespondentsRequest {
pub document_ids: Vec<Uuid>,
pub assignments: Vec<CorrespondentAssignmentInput>,
@@ -267,12 +270,13 @@ pub struct BulkCorrespondentsRequest {
pub action: BulkCorrespondentAction,
}
#[derive(Deserialize)]
#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct CorrespondentRoleQuery {
pub role: String,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct BulkReanalyzeSelectionRequest {
pub document_ids: Vec<Uuid>,
#[serde(default = "default_true")]
@@ -296,17 +300,28 @@ struct UploadOutcome {
created: bool,
}
#[derive(Deserialize)]
#[derive(ToSchema)]
pub struct UploadDocumentForm {
#[schema(value_type = String, format = Binary)]
pub file: String,
#[schema(nullable)]
pub folder_id: Option<Uuid>,
#[schema(nullable)]
pub metadata: Option<Value>,
}
#[derive(Deserialize, ToSchema)]
pub struct MoveDocumentRequest {
pub folder_id: Option<Uuid>,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub struct AssignTagsRequest {
pub tag_ids: Vec<Uuid>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct AssetObjectsQuery {
#[serde(default)]
pub start: Option<i32>,
+17 -1
View File
@@ -2,12 +2,15 @@ use axum::http::HeaderValue;
use axum::{
extract::DefaultBodyLimit,
middleware,
response::Json,
routing::{delete, get, patch, post},
Router,
};
use std::sync::Arc;
use tower_http::cors::{AllowOrigin, CorsLayer};
use utoipa::OpenApi;
use crate::{auth::AuthenticatedUser, state::AppState};
use crate::{auth::AuthenticatedUser, openapi::ApiDoc, state::AppState};
pub mod auth;
pub mod correspondents;
@@ -128,9 +131,22 @@ pub fn create_router(state: AppState) -> Router<()> {
.nest("/api/assets", assets_routes)
.layer(middleware::from_extractor_with_state::<AuthenticatedUser, _>(protected_state));
let openapi_arc = Arc::new(ApiDoc::openapi());
let docs_route = Router::new().route(
"/api/docs/openapi.json",
get({
let spec = openapi_arc.clone();
move || {
let spec = spec.clone();
async move { Json((*spec).clone()) }
}
}),
);
Router::new()
.merge(download_routes)
.merge(protected_routes)
.merge(docs_route)
.nest("/api/auth", auth_routes)
.route("/api/health", get(health::health_check))
.with_state(state)