From 5b5916ff92e1c62b9cf5600b5cff0f4bc292b91c Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 31 Oct 2025 00:39:22 +0100 Subject: [PATCH] openapi cleanup --- backend/src/documents/asset.rs | 8 + backend/src/documents/correspondents.rs | 1 + backend/src/openapi.rs | 618 +++--------------------- backend/src/routes/auth.rs | 29 +- backend/src/routes/correspondents.rs | 13 +- backend/src/routes/documents.rs | 27 +- backend/src/routes/folders.rs | 67 +-- backend/src/routes/profile.rs | 16 +- backend/src/routes/tags.rs | 45 +- 9 files changed, 204 insertions(+), 620 deletions(-) diff --git a/backend/src/documents/asset.rs b/backend/src/documents/asset.rs index 07d986b..f75657d 100644 --- a/backend/src/documents/asset.rs +++ b/backend/src/documents/asset.rs @@ -18,8 +18,10 @@ pub struct DocumentAssetResponse { pub id: Uuid, pub asset_type: String, pub mime_type: String, + #[schema(value_type = Object)] pub metadata: Value, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub cardinality: Option, } @@ -27,10 +29,13 @@ pub struct DocumentAssetResponse { pub struct DocumentAssetObjectResponse { pub id: Uuid, pub ordinal: i32, + #[schema(value_type = Object)] pub metadata: Value, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub url: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub expires_at: Option, } @@ -39,9 +44,11 @@ pub struct DocumentAssetDetailResponse { pub id: Uuid, pub asset_type: String, pub mime_type: String, + #[schema(value_type = Object)] pub metadata: Value, pub created_at: String, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub cardinality: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub objects: Vec, @@ -54,6 +61,7 @@ pub struct DocumentVersionResponse { pub size_bytes: i64, pub checksum: String, pub created_at: String, + #[schema(value_type = Object)] pub metadata: Value, } diff --git a/backend/src/documents/correspondents.rs b/backend/src/documents/correspondents.rs index 93ee604..5c9976e 100644 --- a/backend/src/documents/correspondents.rs +++ b/backend/src/documents/correspondents.rs @@ -16,6 +16,7 @@ use crate::utils::time::to_iso; pub struct DocumentCorrespondentResponse { pub id: Uuid, pub name: String, + #[schema(value_type = Object)] pub metadata: Value, pub assigned_at: String, } diff --git a/backend/src/openapi.rs b/backend/src/openapi.rs index 8cef517..f9ddc14 100644 --- a/backend/src/openapi.rs +++ b/backend/src/openapi.rs @@ -1,7 +1,4 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use utoipa::{IntoParams, OpenApi, ToSchema}; -use uuid::Uuid; +use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( @@ -68,68 +65,72 @@ use uuid::Uuid; schemas::SignupStartRequest, schemas::SignupStartResponse, schemas::SignupFinishRequest, - schemas::AccessTokenResponse, + schemas::LoginResponse, + schemas::LoginResponseVariants, schemas::TenantSnippet, schemas::TenantSelectionResponse, schemas::TenantSelectionRequest, schemas::TenantListResponse, - schemas::LoginResponseVariants, schemas::RegistrationChallengeResponse, schemas::AuthenticationChallengeResponse, schemas::PasskeySummary, schemas::PasskeyRegistrationFinishPayload, schemas::PasskeyLoginStartPayload, schemas::PasskeyLoginFinishPayload, + schemas::DocumentListQuery, + schemas::DocumentStatusFilter, schemas::DocumentResponse, schemas::DocumentDetailResponse, + schemas::DocumentMetadataUpdate, schemas::DocumentVersionResponse, schemas::DocumentVersionDetailResponse, - schemas::DocumentAssetSummary, - schemas::DocumentAssetDetail, - schemas::DocumentAssetObject, + schemas::DocumentAssetResponse, + schemas::DocumentAssetDetailResponse, + schemas::DocumentAssetObjectResponse, schemas::DocumentTypeResponse, schemas::CreateDocumentTypeRequest, schemas::UpdateDocumentTypeRequest, - schemas::DocumentCorrespondent, - schemas::DocumentTag, + schemas::DocumentCorrespondentResponse, + schemas::TagResponse, schemas::UpdateDocumentRequest, - schemas::BulkMoveDocumentsRequest, - schemas::BulkMoveDocumentsResponse, + schemas::RestoreDocumentRequest, + schemas::BulkMoveRequest, + schemas::BulkMoveResponse, schemas::AssignTagsRequest, schemas::MoveDocumentRequest, + schemas::BulkTagAction, schemas::BulkTagRequest, schemas::BulkTagResponse, - schemas::CorrespondentAssignment, - schemas::BulkTagAction, - schemas::BulkCorrespondentsRequest, - schemas::BulkCorrespondentsResponse, - schemas::BulkCorrespondentAction, + schemas::CorrespondentAssignmentInput, schemas::AssignCorrespondentsRequest, - schemas::ReanalyzeRequest, - schemas::ReanalyzeResponse, - schemas::DocumentAssetRequestParams, + schemas::BulkCorrespondentAction, + schemas::BulkCorrespondentsRequest, + schemas::BulkCorrespondentResponse, + schemas::BulkReanalyzeSelectionRequest, + schemas::BulkReanalyzeResponse, + schemas::AssetRequestQuery, schemas::AssetObjectsQuery, schemas::DocumentCheckQuery, schemas::DocumentCheckResponse, schemas::UploadDocumentForm, schemas::CreateFolderRequest, schemas::EnsureFolderPathRequest, - schemas::FolderResponse, schemas::FolderInfo, + schemas::FolderResponse, + schemas::FolderContentsQuery, schemas::FolderContentsResponse, - schemas::FolderDocumentSummary, schemas::UpdateFolderRequest, - schemas::FolderContentsParams, schemas::TagCatalogEntry, schemas::CreateTagRequest, schemas::UpdateTagRequest, schemas::CorrespondentUsage, - schemas::CorrespondentCatalogEntry, + schemas::CorrespondentSummary, schemas::CreateCorrespondentRequest, schemas::UpdateCorrespondentRequest, schemas::WebdavTokenResponse, schemas::WebdavTokenCreatedResponse, schemas::CreateWebdavTokenRequest, + schemas::RevokePasskeyQuery, ) ), tags( @@ -418,8 +419,8 @@ mod doc { #[utoipa::path( post, path = "/api/documents/bulk/move", - request_body = BulkMoveDocumentsRequest, - responses((status = 200, description = "Bulk move outcome", body = BulkMoveDocumentsResponse)), + request_body = BulkMoveRequest, + responses((status = 200, description = "Bulk move outcome", body = BulkMoveResponse)), tag = "Documents" )] pub(super) fn bulk_move_documents() {} @@ -437,7 +438,7 @@ mod doc { post, path = "/api/documents/bulk/correspondents", request_body = BulkCorrespondentsRequest, - responses((status = 200, description = "Bulk correspondents outcome", body = BulkCorrespondentsResponse)), + responses((status = 200, description = "Bulk correspondents outcome", body = BulkCorrespondentResponse)), tag = "Documents" )] pub(super) fn bulk_assign_correspondents() {} @@ -467,8 +468,8 @@ mod doc { #[utoipa::path( post, path = "/api/documents/bulk/reanalyze", - request_body = ReanalyzeRequest, - responses((status = 200, description = "Reanalyze queued", body = ReanalyzeResponse)), + request_body = BulkReanalyzeSelectionRequest, + responses((status = 200, description = "Reanalyze queued", body = BulkReanalyzeResponse)), tag = "Documents" )] pub(super) fn reanalyze_selected_documents() {} @@ -477,7 +478,7 @@ mod doc { get, path = "/api/documents/{id}/assets", params(("id" = Uuid, Path, description = "Document ID")), - responses((status = 200, description = "Document assets", body = [DocumentAssetSummary])), + responses((status = 200, description = "Document assets", body = [DocumentAssetResponse])), tag = "Assets" )] pub(super) fn list_document_assets() {} @@ -487,7 +488,7 @@ mod doc { path = "/api/documents/{id}/assets", params( ("id" = Uuid, Path, description = "Document ID"), - DocumentAssetRequestParams + AssetRequestQuery ), responses((status = 202, description = "Asset generation requested")), tag = "Assets" @@ -501,7 +502,7 @@ mod doc { ("asset_id" = Uuid, Path, description = "Asset ID"), AssetObjectsQuery ), - responses((status = 200, description = "Asset detail", body = DocumentAssetDetail)), + responses((status = 200, description = "Asset detail", body = DocumentAssetDetailResponse)), tag = "Assets" )] pub(super) fn get_document_asset() {} @@ -541,7 +542,7 @@ mod doc { path = "/api/folders/{id}/contents", params( ("id" = Uuid, Path, description = "Folder ID"), - FolderContentsParams + FolderContentsQuery ), responses((status = 200, description = "Folder contents", body = FolderContentsResponse)), tag = "Folders" @@ -735,525 +736,42 @@ mod tests { } pub mod schemas { - use super::*; - pub use crate::auth::passkeys::{ AuthenticationChallengeResponse, PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload, PasskeySummary, RegistrationChallengeResponse, }; - use webauthn_rs::prelude::RegisterPublicKeyCredential; - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct LoginRequest { - pub username: String, - pub password: String, - #[schema(nullable)] - pub preferred_tenant_id: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct SignupStartRequest { - pub username: String, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct SignupStartResponse { - pub signup_token: String, - pub challenge: RegistrationChallengeResponse, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct SignupFinishRequest { - pub signup_token: String, - #[schema(value_type = Object)] - pub credential: RegisterPublicKeyCredential, - #[schema(nullable)] - pub nickname: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct AccessTokenResponse { - pub access_token: String, - pub token_type: String, - pub expires_in: i64, - pub tenant: TenantSnippet, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct TenantSnippet { - pub id: Uuid, - pub name: String, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct TenantSelectionResponse { - pub access_token: String, - pub tenants: Vec, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct TenantSelectionRequest { - pub tenant_id: Uuid, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct TenantListResponse { - pub tenants: Vec, - } - - #[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, - #[schema(nullable)] - pub include_descendants: Option, - pub query: Option, - pub tags: Option, - pub correspondents: Option, - pub document_types: Option, - #[serde(default = "default_document_status_filter")] - #[schema(default = "active")] - pub status: DocumentStatusFilter, - } - - fn default_document_status_filter() -> DocumentStatusFilter { - DocumentStatusFilter::Active - } - - #[derive(Serialize, Deserialize, ToSchema)] - #[serde(rename_all = "lowercase")] - pub enum DocumentStatusFilter { - Active, - Deleted, - All, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentTag { - pub id: Uuid, - pub label: String, - #[schema(nullable)] - pub color: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentAssetObject { - pub id: Uuid, - pub ordinal: i32, - pub metadata: Value, - #[schema(nullable)] - pub url: Option, - #[schema(nullable)] - pub expires_at: Option, - } - - #[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, - } - - #[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, - pub objects: Vec, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentVersionResponse { - pub id: Uuid, - pub version_number: i32, - pub size_bytes: i64, - pub checksum: String, - pub created_at: String, - pub metadata: Value, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentVersionDetailResponse { - #[serde(flatten)] - pub version: DocumentVersionResponse, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub assets: Vec, - pub download_path: String, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentCorrespondent { - pub id: Uuid, - pub name: String, - pub metadata: Value, - pub assigned_at: String, - } - + pub use crate::documents::asset::{ + DocumentAssetDetailResponse, DocumentAssetObjectResponse, DocumentAssetResponse, + DocumentVersionDetailResponse, DocumentVersionResponse, + }; + pub use crate::documents::correspondents::DocumentCorrespondentResponse; + pub use crate::routes::auth::{ + LoginRequest, LoginResponse, LoginResponseVariants, SignupFinishRequest, + SignupStartRequest, SignupStartResponse, TenantListResponse, TenantSelectionRequest, + TenantSelectionResponse, TenantSnippet, + }; + pub use crate::routes::correspondents::{ + CorrespondentSummary, CorrespondentUsage, CreateCorrespondentRequest, + UpdateCorrespondentRequest, + }; pub use crate::routes::document_types::{CreateDocumentTypeRequest, UpdateDocumentTypeRequest}; - pub use crate::routes::documents::DocumentTypeResponse; - - #[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, - #[schema(nullable)] - pub folder_id: Option, - pub created_at: String, - pub updated_at: String, - #[schema(nullable)] - pub deleted_at: Option, - #[schema(nullable)] - pub issued_at: Option, - pub metadata: Value, - #[schema(nullable)] - pub document_type_id: Option, - #[schema(nullable)] - pub document_type: Option, - pub tags: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub correspondents: Vec, - #[schema(nullable)] - pub current_version: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentDetailResponse { - pub document: DocumentResponse, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentMetadataUpdate { - pub value: Value, - #[serde(default)] - #[schema(default = false)] - pub replace: bool, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct RestoreDocumentRequest { - #[schema(nullable)] - pub folder_id: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct UpdateDocumentRequest { - #[schema(nullable)] - pub title: Option, - #[schema(nullable, value_type = Option)] - pub issued_at: Option, - #[schema(nullable)] - pub metadata: Option, - #[schema(nullable)] - pub document_type_id: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct BulkMoveDocumentsRequest { - pub document_ids: Vec, - #[schema(nullable)] - pub folder_id: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct BulkMoveDocumentsResponse { - pub updated: usize, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct AssignTagsRequest { - pub tag_ids: Vec, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct MoveDocumentRequest { - #[schema(nullable)] - pub folder_id: Option, - } - - #[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, - pub tag_ids: Vec, - 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, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct AssignCorrespondentsRequest { - pub assignments: Vec, - #[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, - pub assignments: Vec, - #[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, ToSchema)] - pub struct ReanalyzeRequest { - pub document_ids: Vec, - #[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, - } - - #[derive(Serialize, Deserialize, IntoParams, ToSchema)] - #[into_params(parameter_in = Query)] - pub struct AssetObjectsQuery { - #[serde(default)] - pub start: Option, - #[serde(default)] - pub limit: Option, - } - - #[derive(Serialize, Deserialize, IntoParams, ToSchema)] - #[into_params(parameter_in = Query)] - pub struct DocumentCheckQuery { - pub checksum: String, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct DocumentCheckResponse { - pub exists: bool, - #[schema(nullable)] - pub document_id: Option, - #[schema(nullable)] - pub title: Option, - #[schema(nullable)] - pub filename: Option, - #[schema(nullable)] - pub version_id: Option, - #[schema(nullable)] - pub version_number: Option, - #[schema(nullable)] - pub created_at: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct UploadDocumentForm { - #[schema(value_type = String, format = Binary)] - pub file: String, - #[schema(nullable)] - pub folder_id: Option, - #[schema(nullable)] - pub metadata: Option, - #[schema(nullable)] - pub title: Option, - #[schema(nullable, value_type = Vec)] - pub tag_ids: Option>, - #[schema(nullable, value_type = Vec)] - pub correspondents: Option>, - #[schema(nullable, example = "2024-01-01T00:00:00Z")] - pub issued_at: Option, - #[schema(nullable)] - pub skip_existing: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct CreateFolderRequest { - pub name: String, - #[schema(nullable)] - pub parent_id: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct EnsureFolderPathRequest { - #[schema(nullable)] - pub parent_id: Option, - pub segments: Vec, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct FolderInfo { - pub id: Uuid, - pub name: String, - #[schema(nullable)] - pub parent_id: Option, - 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, - } - - #[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, - pub subfolders: Vec, - pub documents: Vec, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct UpdateFolderRequest { - #[schema(nullable)] - pub parent_id: Option>, - #[schema(nullable)] - pub name: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct TagCatalogEntry { - pub id: Uuid, - pub label: String, - #[schema(nullable)] - pub color: Option, - pub usage_count: i64, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct CreateTagRequest { - pub label: String, - #[schema(nullable)] - pub color: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct UpdateTagRequest { - #[schema(nullable)] - pub label: Option>, - #[schema(nullable)] - pub color: Option>, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct CorrespondentUsage { - pub total: i64, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct CorrespondentCatalogEntry { - pub id: Uuid, - pub name: String, - pub metadata: Value, - pub created_at: String, - pub updated_at: String, - pub usage: CorrespondentUsage, - } - - #[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, - #[schema(nullable)] - pub metadata: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct WebdavTokenResponse { - pub id: Uuid, - pub tenant_id: Uuid, - #[schema(nullable)] - pub label: Option, - pub created_at: String, - #[schema(nullable)] - pub last_used_at: Option, - #[schema(nullable)] - pub expires_at: Option, - #[schema(nullable)] - pub revoked_at: Option, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct WebdavTokenCreatedResponse { - pub token: String, - pub token_info: WebdavTokenResponse, - } - - #[derive(Serialize, Deserialize, ToSchema)] - pub struct CreateWebdavTokenRequest { - #[schema(nullable)] - pub label: Option, - #[schema(nullable, example = "2025-01-01T00:00:00Z")] - pub expires_at: Option, - } + pub use crate::routes::documents::{ + AssetObjectsQuery, AssetRequestQuery, AssignCorrespondentsRequest, AssignTagsRequest, + BulkCorrespondentAction, BulkCorrespondentResponse, BulkCorrespondentsRequest, + BulkMoveRequest, BulkMoveResponse, BulkReanalyzeResponse, BulkReanalyzeSelectionRequest, + BulkTagAction, BulkTagRequest, BulkTagResponse, CorrespondentAssignmentInput, + DocumentCheckQuery, DocumentCheckResponse, DocumentDetailResponse, DocumentListQuery, + DocumentMetadataUpdate, DocumentResponse, DocumentStatusFilter, DocumentTypeResponse, + MoveDocumentRequest, RestoreDocumentRequest, TagResponse, UpdateDocumentRequest, + UploadDocumentForm, + }; + pub use crate::routes::folders::{ + CreateFolderRequest, EnsureFolderPathRequest, FolderContentsQuery, FolderContentsResponse, + FolderInfo, FolderResponse, UpdateFolderRequest, + }; + pub use crate::routes::profile::{ + CreateWebdavTokenRequest, RevokePasskeyQuery, WebdavTokenCreatedResponse, + WebdavTokenResponse, + }; + pub use crate::routes::tags::{CreateTagRequest, TagCatalogEntry, UpdateTagRequest}; } diff --git a/backend/src/routes/auth.rs b/backend/src/routes/auth.rs index c0c9ffd..45a4cc7 100644 --- a/backend/src/routes/auth.rs +++ b/backend/src/routes/auth.rs @@ -13,6 +13,7 @@ use diesel::{pg::PgConnection, prelude::*}; use rand::{rngs::OsRng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use utoipa::ToSchema; use uuid::Uuid; use crate::{ @@ -37,15 +38,16 @@ use webauthn_rs::prelude::RegisterPublicKeyCredential; const REFRESH_COOKIE_NAME: &str = "refresh_token"; -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct LoginRequest { pub username: String, pub password: String, #[serde(default)] + #[schema(nullable)] pub preferred_tenant_id: Option, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct LoginResponse { pub access_token: String, pub token_type: String, @@ -53,46 +55,55 @@ pub struct LoginResponse { pub tenant: TenantSnippet, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct TenantSnippet { pub id: Uuid, pub name: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct TenantSelectionResponse { pub access_token: String, pub tenants: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct TenantListResponse { pub tenants: Vec, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct TenantSelectionRequest { pub tenant_id: Uuid, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct SignupStartRequest { pub username: String, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct SignupStartResponse { pub signup_token: String, pub challenge: RegistrationChallengeResponse, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct SignupFinishRequest { pub signup_token: String, + #[schema(value_type = Object)] pub credential: RegisterPublicKeyCredential, + #[schema(nullable)] pub nickname: Option, } +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(untagged)] +pub enum LoginResponseVariants { + Token(LoginResponse), + Selection(TenantSelectionResponse), +} + pub async fn login(_state: State, _payload: Json) -> AppResult { Err(AppError::bad_request( "password authentication is no longer supported", diff --git a/backend/src/routes/correspondents.rs b/backend/src/routes/correspondents.rs index 7e730c6..c8da22a 100644 --- a/backend/src/routes/correspondents.rs +++ b/backend/src/routes/correspondents.rs @@ -5,6 +5,7 @@ use chrono::Utc; use diesel::{dsl::count_star, prelude::*, result::DatabaseErrorKind, PgConnection}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use utoipa::ToSchema; use uuid::Uuid; use crate::{ @@ -18,31 +19,35 @@ use crate::{ }, }; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct CorrespondentUsage { pub total: i64, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct CorrespondentSummary { pub id: Uuid, pub name: String, + #[schema(value_type = Object)] pub metadata: Value, pub created_at: String, pub updated_at: String, pub usage: CorrespondentUsage, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct CreateCorrespondentRequest { pub name: String, #[serde(default)] + #[schema(nullable, value_type = Object)] pub metadata: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct UpdateCorrespondentRequest { + #[schema(nullable)] pub name: Option, + #[schema(nullable, value_type = Object)] pub metadata: Option, } diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index 4c0b003..a0af0a5 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -60,14 +60,17 @@ const PRESIGNED_URL_EXPIRY_SECONDS: u64 = 300; #[derive(Deserialize, IntoParams, ToSchema)] #[into_params(parameter_in = Query)] pub struct DocumentListQuery { + #[schema(nullable)] pub folder_id: Option, #[serde(default)] + #[schema(nullable)] pub include_descendants: Option, pub query: Option, pub tags: Option, pub correspondents: Option, pub document_types: Option, #[serde(default = "default_document_status_filter")] + #[schema(default = "active")] pub status: DocumentStatusFilter, } @@ -87,6 +90,7 @@ pub enum DocumentStatusFilter { #[into_params(parameter_in = Query)] pub struct AssetRequestQuery { #[serde(default)] + #[schema(default = false)] pub force: bool, } @@ -100,16 +104,22 @@ pub struct DocumentCheckQuery { pub struct DocumentCheckResponse { pub exists: bool, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub document_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub title: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub filename: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub version_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub version_number: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub created_at: Option, } @@ -117,6 +127,7 @@ pub struct DocumentCheckResponse { pub struct TagResponse { pub id: Uuid, pub label: String, + #[schema(nullable)] pub color: Option, } @@ -151,21 +162,29 @@ pub struct DocumentResponse { pub filename: String, pub title: String, pub original_name: String, + #[schema(nullable)] pub content_type: Option, + #[schema(nullable)] pub folder_id: Option, pub created_at: String, pub updated_at: String, + #[schema(nullable)] pub deleted_at: Option, + #[schema(nullable)] pub issued_at: Option, + #[schema(value_type = Object)] pub metadata: Value, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub document_type_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub document_type: Option, pub tags: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub correspondents: Vec, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable)] pub current_version: Option, } #[derive(Serialize, ToSchema)] @@ -181,11 +200,13 @@ pub struct BulkReanalyzeResponse { #[derive(Deserialize, ToSchema)] pub struct BulkMoveRequest { pub document_ids: Vec, + #[schema(nullable)] pub folder_id: Option, } #[derive(Deserialize, ToSchema)] pub struct DocumentMetadataUpdate { + #[schema(value_type = Object)] pub value: Value, #[serde(default)] #[schema(default = false)] @@ -200,7 +221,7 @@ pub struct UpdateDocumentRequest { #[schema(nullable, value_type = Option)] pub issued_at: Option, #[serde(default)] - #[schema(nullable)] + #[schema(nullable, value_type = Object)] pub metadata: Option, #[serde(default)] #[schema(nullable, value_type = Option)] @@ -247,6 +268,7 @@ pub struct CorrespondentAssignmentInput { pub struct AssignCorrespondentsRequest { pub assignments: Vec, #[serde(default)] + #[schema(default = false)] pub replace: bool, } @@ -284,6 +306,7 @@ pub struct BulkCorrespondentsRequest { pub struct BulkReanalyzeSelectionRequest { pub document_ids: Vec, #[serde(default = "default_true")] + #[schema(default = true)] pub force: bool, } @@ -316,7 +339,7 @@ pub struct UploadDocumentForm { pub file: String, #[schema(nullable)] pub folder_id: Option, - #[schema(nullable)] + #[schema(nullable, value_type = Object)] pub metadata: Option, #[schema(nullable)] pub title: Option, diff --git a/backend/src/routes/folders.rs b/backend/src/routes/folders.rs index e808666..a407e68 100644 --- a/backend/src/routes/folders.rs +++ b/backend/src/routes/folders.rs @@ -6,7 +6,7 @@ use axum::{ }; use diesel::{dsl::exists, prelude::*, PgConnection}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use crate::models::{Document, DocumentType, Folder, NewFolder}; @@ -22,38 +22,40 @@ use crate::documents::{ asset::load_primary_assets, correspondents::load_correspondents_for_documents, tags::load_tags_for_documents, }; -use crate::utils::{ - json::{classify_nullable, NullableValue}, - time::to_iso, -}; +use crate::utils::time::to_iso; -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct CreateFolderRequest { pub name: String, + #[schema(nullable)] pub parent_id: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct EnsureFolderPathRequest { + #[schema(nullable)] pub parent_id: Option, pub segments: Vec, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct FolderResponse { pub folder: FolderInfo, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct FolderContentsResponse { + #[schema(nullable)] pub folder: Option, pub subfolders: Vec, pub documents: Vec, } -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams, ToSchema)] +#[into_params(parameter_in = Query)] pub struct FolderContentsQuery { #[serde(default = "default_include_documents")] + #[schema(default = true)] pub include_documents: bool, } @@ -61,15 +63,27 @@ const fn default_include_documents() -> bool { true } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct FolderInfo { pub id: Uuid, pub name: String, + #[schema(nullable)] pub parent_id: Option, pub created_at: String, pub updated_at: String, } +#[derive(Default, Deserialize, ToSchema)] +#[serde(default)] +pub struct UpdateFolderRequest { + #[serde(default)] + #[schema(nullable, value_type = Option)] + pub parent_id: Option>, + #[serde(default)] + #[schema(nullable)] + pub name: Option>, +} + pub async fn get_folder( Path(folder_id): Path, TenantScopedConn { @@ -435,15 +449,8 @@ pub async fn update_folder( tenant_id, .. }: TenantScopedConn, - Json(body): Json, + Json(payload): Json, ) -> AppResult { - if !body.is_object() { - return Err(AppError::bad_request("request body must be a JSON object")); - } - - let parent_class = classify_nullable(body.get("parent_id")).map_err(AppError::bad_request)?; - let name_class = classify_nullable(body.get("name")).map_err(AppError::bad_request)?; - conn.transaction::<(), AppError, _>(|conn| { let folder: Folder = folders::table .find(folder_id) @@ -452,21 +459,15 @@ pub async fn update_folder( let mut next_parent = folder.parent_id; let mut parent_changed = false; - match parent_class { - NullableValue::Omitted => {} - NullableValue::Null => { + match payload.parent_id { + None => {} + Some(None) => { if folder.parent_id.is_some() { parent_changed = true; } next_parent = None; } - NullableValue::String(value) => { - let trimmed = value.trim(); - if trimmed.is_empty() { - return Err(AppError::bad_request("parent_id must not be empty")); - } - let parent_id = Uuid::parse_str(trimmed) - .map_err(|_| AppError::bad_request("parent_id must be a valid UUID or null"))?; + Some(Some(parent_id)) => { if parent_id == folder_id { return Err(AppError::bad_request("folder cannot be its own parent")); } @@ -492,12 +493,12 @@ pub async fn update_folder( let mut new_name = folder.name.clone(); let mut name_changed = false; - match name_class { - NullableValue::Omitted => {} - NullableValue::Null => { + match payload.name { + None => {} + Some(None) => { return Err(AppError::bad_request("name cannot be null")); } - NullableValue::String(value) => { + Some(Some(value)) => { let trimmed = value.trim(); if trimmed.is_empty() { return Err(AppError::bad_request("name must not be empty")); diff --git a/backend/src/routes/profile.rs b/backend/src/routes/profile.rs index 990ceff..4eb7033 100644 --- a/backend/src/routes/profile.rs +++ b/backend/src/routes/profile.rs @@ -5,6 +5,7 @@ use axum::{ }; use chrono::{DateTime, NaiveDateTime}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use uuid::Uuid; use crate::auth::{ @@ -20,32 +21,39 @@ use crate::models::WebdavToken; use crate::state::AppState; use crate::utils::{db::no_content, time::to_iso}; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct WebdavTokenResponse { pub id: Uuid, pub tenant_id: Uuid, + #[schema(nullable)] pub label: Option, pub created_at: String, + #[schema(nullable)] pub last_used_at: Option, + #[schema(nullable)] pub expires_at: Option, + #[schema(nullable)] pub revoked_at: Option, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct WebdavTokenCreatedResponse { pub token: String, pub token_info: WebdavTokenResponse, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct CreateWebdavTokenRequest { + #[schema(nullable)] pub label: Option, + #[schema(nullable)] pub expires_at: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, ToSchema)] pub struct RevokePasskeyQuery { #[serde(default)] + #[schema(nullable)] pub reason: Option, } diff --git a/backend/src/routes/tags.rs b/backend/src/routes/tags.rs index 5df7ef3..d60db03 100644 --- a/backend/src/routes/tags.rs +++ b/backend/src/routes/tags.rs @@ -1,9 +1,8 @@ -use crate::utils::json::{classify_nullable, NullableValue}; use axum::{extract::Path, http::StatusCode, Json}; use diesel::{dsl::count_star, prelude::*}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::HashMap; +use utoipa::ToSchema; use uuid::Uuid; use crate::auth::TenantScopedConn; @@ -12,9 +11,10 @@ use crate::models::{NewTag, Tag}; use crate::schema::{document_tags, tags}; use crate::utils::db::{no_content, EnsureEntity, IntoJsonResponse}; -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct CreateTagRequest { pub label: String, + #[schema(nullable)] pub color: Option, } @@ -25,14 +25,26 @@ struct UpdateTagChangeset<'a> { color: Option>, } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct TagCatalogEntry { pub id: Uuid, pub label: String, + #[schema(nullable)] pub color: Option, pub usage_count: i64, } +#[derive(Debug, Default, Deserialize, ToSchema)] +#[serde(default)] +pub struct UpdateTagRequest { + #[serde(default)] + #[schema(nullable, value_type = Option)] + pub label: Option>, + #[serde(default)] + #[schema(nullable, value_type = Option)] + pub color: Option>, +} + pub async fn list_tags( TenantScopedConn { mut conn, @@ -121,19 +133,16 @@ pub async fn update_tag( tenant_id, .. }: TenantScopedConn, - Json(body): Json, + Json(payload): Json, ) -> AppResult> { let existing: Tag = tags::table .find(tag_id) .filter(tags::tenant_id.eq(tenant_id)) .first(&mut conn) .one()?; - let label_class = classify_nullable(body.get("label")).map_err(AppError::bad_request)?; - let color_class = classify_nullable(body.get("color")).map_err(AppError::bad_request)?; + let UpdateTagRequest { label, color } = payload; - if matches!(label_class, NullableValue::Omitted) - && matches!(color_class, NullableValue::Omitted) - { + if label.is_none() && color.is_none() { let usage_count: i64 = document_tags::table .filter(document_tags::tag_id.eq(tag_id)) .select(count_star()) @@ -149,12 +158,12 @@ pub async fn update_tag( let mut new_label: Option = None; let mut label_changed = false; - match label_class { - NullableValue::Omitted => {} - NullableValue::Null => { + match label { + None => {} + Some(None) => { return Err(AppError::bad_request("label cannot be null")); } - NullableValue::String(value) => { + Some(Some(value)) => { let trimmed = value.trim(); if trimmed.is_empty() { return Err(AppError::bad_request("label must not be empty")); @@ -177,13 +186,13 @@ pub async fn update_tag( let mut color_change: Option> = None; let mut color_changed = false; - match color_class { - NullableValue::Omitted => {} - NullableValue::Null => { + match color { + None => {} + Some(None) => { color_change = Some(None); color_changed = true; } - NullableValue::String(value) => { + Some(Some(value)) => { let trimmed = value.trim(); if trimmed.is_empty() { return Err(AppError::bad_request("color must not be empty"));