openapi cleanup

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