cleanup
This commit is contained in:
@@ -65,7 +65,7 @@ CREATE TABLE documents (
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
content_type VARCHAR(100),
|
||||
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
|
||||
@@ -7,6 +7,7 @@ use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
|
||||
use axum_extra::headers::{authorization::Bearer, Authorization};
|
||||
use axum_extra::TypedHeader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
error::AppError,
|
||||
@@ -14,7 +15,7 @@ use crate::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AuthenticatedUser {
|
||||
pub user_id: uuid::Uuid,
|
||||
pub username: String,
|
||||
|
||||
+67
-727
@@ -1,737 +1,64 @@
|
||||
use utoipa::openapi::{self, tag::TagBuilder, InfoBuilder};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
doc::health_check,
|
||||
doc::login,
|
||||
doc::signup_start,
|
||||
doc::signup_finish,
|
||||
doc::refresh,
|
||||
doc::logout,
|
||||
doc::me,
|
||||
doc::list_tenants,
|
||||
doc::select_tenant,
|
||||
doc::passkey_register_start,
|
||||
doc::passkey_register_finish,
|
||||
doc::passkey_login_start,
|
||||
doc::passkey_login_finish,
|
||||
doc::list_documents,
|
||||
doc::check_document,
|
||||
doc::upload_document,
|
||||
doc::get_document,
|
||||
doc::update_document,
|
||||
doc::delete_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_document_types,
|
||||
doc::create_document_type,
|
||||
doc::update_document_type,
|
||||
doc::delete_document_type,
|
||||
doc::list_correspondents,
|
||||
doc::create_correspondent,
|
||||
doc::update_correspondent,
|
||||
doc::delete_correspondent,
|
||||
doc::list_webdav_tokens,
|
||||
doc::create_webdav_token,
|
||||
doc::delete_webdav_token,
|
||||
doc::list_passkeys,
|
||||
doc::delete_passkey,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
schemas::LoginRequest,
|
||||
schemas::SignupStartRequest,
|
||||
schemas::SignupStartResponse,
|
||||
schemas::SignupFinishRequest,
|
||||
schemas::LoginResponse,
|
||||
schemas::LoginResponseVariants,
|
||||
schemas::TenantSnippet,
|
||||
schemas::TenantSelectionResponse,
|
||||
schemas::TenantSelectionRequest,
|
||||
schemas::TenantListResponse,
|
||||
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::DocumentAssetResponse,
|
||||
schemas::DocumentAssetDetailResponse,
|
||||
schemas::DocumentAssetObjectResponse,
|
||||
schemas::DocumentTypeResponse,
|
||||
schemas::CreateDocumentTypeRequest,
|
||||
schemas::UpdateDocumentTypeRequest,
|
||||
schemas::DocumentCorrespondentResponse,
|
||||
schemas::TagResponse,
|
||||
schemas::UpdateDocumentRequest,
|
||||
schemas::RestoreDocumentRequest,
|
||||
schemas::BulkMoveRequest,
|
||||
schemas::BulkMoveResponse,
|
||||
schemas::AssignTagsRequest,
|
||||
schemas::MoveDocumentRequest,
|
||||
schemas::BulkTagAction,
|
||||
schemas::BulkTagRequest,
|
||||
schemas::BulkTagResponse,
|
||||
schemas::CorrespondentAssignmentInput,
|
||||
schemas::AssignCorrespondentsRequest,
|
||||
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::FolderInfo,
|
||||
schemas::FolderResponse,
|
||||
schemas::FolderContentsQuery,
|
||||
schemas::FolderContentsResponse,
|
||||
schemas::UpdateFolderRequest,
|
||||
schemas::TagCatalogEntry,
|
||||
schemas::CreateTagRequest,
|
||||
schemas::UpdateTagRequest,
|
||||
schemas::CorrespondentUsage,
|
||||
schemas::CorrespondentSummary,
|
||||
schemas::CreateCorrespondentRequest,
|
||||
schemas::UpdateCorrespondentRequest,
|
||||
schemas::WebdavTokenResponse,
|
||||
schemas::WebdavTokenCreatedResponse,
|
||||
schemas::CreateWebdavTokenRequest,
|
||||
schemas::RevokePasskeyQuery,
|
||||
)
|
||||
),
|
||||
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 = "DocumentTypes", description = "Document type catalog"),
|
||||
(name = "Correspondents", description = "Correspondent catalog"),
|
||||
(name = "Profile", description = "User profile and WebDAV tokens")
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod doc {
|
||||
use super::schemas::*;
|
||||
use uuid::Uuid;
|
||||
impl OpenApi for ApiDoc {
|
||||
fn openapi() -> openapi::OpenApi {
|
||||
let mut doc = crate::routes::health::HealthApiDoc::openapi();
|
||||
doc.merge(crate::routes::auth::AuthApiDoc::openapi());
|
||||
doc.merge(crate::routes::documents::DocumentsApiDoc::openapi());
|
||||
doc.merge(crate::routes::folders::FoldersApiDoc::openapi());
|
||||
doc.merge(crate::routes::tags::TagsApiDoc::openapi());
|
||||
doc.merge(crate::routes::document_types::DocumentTypesApiDoc::openapi());
|
||||
doc.merge(crate::routes::correspondents::CorrespondentsApiDoc::openapi());
|
||||
doc.merge(crate::routes::profile::ProfileApiDoc::openapi());
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn __keep_uuid_import() {
|
||||
let _ = Uuid::nil();
|
||||
}
|
||||
doc.info = InfoBuilder::new()
|
||||
.title("Papercrate API")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.build();
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/health",
|
||||
responses((status = 200, description = "Service is healthy")),
|
||||
tag = "Health"
|
||||
)]
|
||||
pub(super) fn health_check() {}
|
||||
doc.tags = Some(vec![
|
||||
TagBuilder::new()
|
||||
.name("Health")
|
||||
.description(Some("Service health"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Auth")
|
||||
.description(Some("Authentication"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Documents")
|
||||
.description(Some("Document management"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Assets")
|
||||
.description(Some("Document assets"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Folders")
|
||||
.description(Some("Folder management"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Tags")
|
||||
.description(Some("Tag catalog"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("DocumentTypes")
|
||||
.description(Some("Document type catalog"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Correspondents")
|
||||
.description(Some("Correspondent catalog"))
|
||||
.build(),
|
||||
TagBuilder::new()
|
||||
.name("Profile")
|
||||
.description(Some("User profile and WebDAV tokens"))
|
||||
.build(),
|
||||
]);
|
||||
|
||||
#[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/signup/start",
|
||||
request_body = SignupStartRequest,
|
||||
responses(
|
||||
(status = 200, description = "Signup challenge created", body = SignupStartResponse),
|
||||
(status = 400, description = "Invalid signup request"),
|
||||
(status = 409, description = "Username already exists")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn signup_start() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/signup/finish",
|
||||
request_body = SignupFinishRequest,
|
||||
responses(
|
||||
(status = 200, description = "Signup completed", body = LoginResponseVariants),
|
||||
(status = 400, description = "Invalid signup completion"),
|
||||
(status = 409, description = "Username already exists")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn signup_finish() {}
|
||||
|
||||
#[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(
|
||||
get,
|
||||
path = "/api/auth/tenants",
|
||||
responses((status = 200, description = "Available tenants", body = TenantListResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn list_tenants() {}
|
||||
|
||||
#[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(
|
||||
post,
|
||||
path = "/api/auth/passkeys/register/start",
|
||||
responses((status = 200, description = "Passkey registration challenge", body = RegistrationChallengeResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn passkey_register_start() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/register/finish",
|
||||
request_body = PasskeyRegistrationFinishPayload,
|
||||
responses((status = 200, description = "Passkey registered", body = PasskeySummary)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn passkey_register_finish() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/login/start",
|
||||
request_body = PasskeyLoginStartPayload,
|
||||
responses((status = 200, description = "Passkey authentication challenge", body = AuthenticationChallengeResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn passkey_login_start() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/login/finish",
|
||||
request_body = PasskeyLoginFinishPayload,
|
||||
responses(
|
||||
(status = 200, description = "Passkey login successful", body = LoginResponseVariants),
|
||||
(status = 401, description = "Authentication failed")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub(super) fn passkey_login_finish() {}
|
||||
|
||||
#[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 created", body = DocumentDetailResponse),
|
||||
(status = 200, description = "Existing document reused", body = DocumentDetailResponse),
|
||||
(status = 204, description = "Upload skipped because the document already exists")
|
||||
),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub(super) fn upload_document() {}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents/check",
|
||||
params(DocumentCheckQuery),
|
||||
responses((status = 200, description = "Checksum lookup", body = DocumentCheckResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub(super) fn check_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}/versions",
|
||||
params(("id" = Uuid, Path, description = "Document ID")),
|
||||
responses((status = 200, description = "Document versions", body = [DocumentVersionResponse])),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub(super) fn list_document_versions() {}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents/{id}/versions/{version_id}",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "Document ID"),
|
||||
("version_id" = Uuid, Path, description = "Version ID"),
|
||||
),
|
||||
responses((status = 200, description = "Document version detail", body = DocumentVersionDetailResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub(super) fn get_document_version() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/{id}/restore",
|
||||
params(("id" = Uuid, Path, description = "Document ID")),
|
||||
request_body = RestoreDocumentRequest,
|
||||
responses((status = 204, description = "Document restored")),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub(super) fn restore_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 = BulkMoveRequest,
|
||||
responses((status = 200, description = "Bulk move outcome", body = BulkMoveResponse)),
|
||||
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 = BulkCorrespondentResponse)),
|
||||
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")
|
||||
),
|
||||
responses((status = 204, description = "Correspondent removed")),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub(super) fn remove_correspondent() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/bulk/reanalyze",
|
||||
request_body = BulkReanalyzeSelectionRequest,
|
||||
responses((status = 200, description = "Reanalyze queued", body = BulkReanalyzeResponse)),
|
||||
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 = [DocumentAssetResponse])),
|
||||
tag = "Assets"
|
||||
)]
|
||||
pub(super) fn list_document_assets() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/{id}/assets",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "Document ID"),
|
||||
AssetRequestQuery
|
||||
),
|
||||
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 = DocumentAssetDetailResponse)),
|
||||
tag = "Assets"
|
||||
)]
|
||||
pub(super) fn get_document_asset() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/folders",
|
||||
request_body = CreateFolderRequest,
|
||||
responses(
|
||||
(status = 201, description = "Folder created", body = FolderResponse),
|
||||
(status = 200, description = "Folder already existed", 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"),
|
||||
FolderContentsQuery
|
||||
),
|
||||
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(
|
||||
get,
|
||||
path = "/api/document-types",
|
||||
responses((status = 200, description = "Document types", body = [DocumentTypeResponse])),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub(super) fn list_document_types() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/document-types",
|
||||
request_body = CreateDocumentTypeRequest,
|
||||
responses((status = 201, description = "Document type created", body = DocumentTypeResponse)),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub(super) fn create_document_type() {}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/document-types/{id}",
|
||||
params(("id" = Uuid, Path, description = "Document type ID")),
|
||||
request_body = UpdateDocumentTypeRequest,
|
||||
responses((status = 200, description = "Document type updated", body = DocumentTypeResponse)),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub(super) fn update_document_type() {}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/document-types/{id}",
|
||||
params(("id" = Uuid, Path, description = "Document type ID")),
|
||||
responses((status = 204, description = "Document type deleted")),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub(super) fn delete_document_type() {}
|
||||
|
||||
#[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() {}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/profile/webdav-tokens",
|
||||
responses((status = 200, description = "List WebDAV tokens", body = [WebdavTokenResponse])),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub(super) fn list_webdav_tokens() {}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/profile/webdav-tokens",
|
||||
request_body = CreateWebdavTokenRequest,
|
||||
responses((status = 201, description = "WebDAV token created", body = WebdavTokenCreatedResponse)),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub(super) fn create_webdav_token() {}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/profile/webdav-tokens/{id}",
|
||||
params(("id" = Uuid, Path, description = "WebDAV token ID")),
|
||||
responses((status = 204, description = "WebDAV token revoked")),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub(super) fn delete_webdav_token() {}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/profile/passkeys",
|
||||
responses((status = 200, description = "List registered passkeys", body = [PasskeySummary])),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub(super) fn list_passkeys() {}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/profile/passkeys/{id}",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "Passkey ID"),
|
||||
("reason" = Option<String>, Query, description = "Optional reason for revoking the passkey")
|
||||
),
|
||||
responses((status = 204, description = "Passkey revoked")),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub(super) fn delete_passkey() {}
|
||||
}
|
||||
|
||||
#[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");
|
||||
doc
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,6 +67,7 @@ pub mod schemas {
|
||||
AuthenticationChallengeResponse, PasskeyLoginFinishPayload, PasskeyLoginStartPayload,
|
||||
PasskeyRegistrationFinishPayload, PasskeySummary, RegistrationChallengeResponse,
|
||||
};
|
||||
pub use crate::auth::AuthenticatedUser;
|
||||
pub use crate::documents::asset::{
|
||||
DocumentAssetDetailResponse, DocumentAssetObjectResponse, DocumentAssetResponse,
|
||||
DocumentVersionDetailResponse, DocumentVersionResponse,
|
||||
@@ -775,3 +103,15 @@ pub mod schemas {
|
||||
};
|
||||
pub use crate::routes::tags::{CreateTagRequest, TagCatalogEntry, UpdateTagRequest};
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
||||
+135
-1
@@ -13,7 +13,7 @@ use diesel::{pg::PgConnection, prelude::*};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use utoipa::ToSchema;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -104,12 +104,71 @@ pub enum LoginResponseVariants {
|
||||
Selection(TenantSelectionResponse),
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
login,
|
||||
signup_start,
|
||||
signup_finish,
|
||||
refresh,
|
||||
logout,
|
||||
me,
|
||||
list_tenants,
|
||||
select_tenant,
|
||||
passkey_register_start,
|
||||
passkey_register_finish,
|
||||
passkey_login_start,
|
||||
passkey_login_finish,
|
||||
),
|
||||
components(schemas(
|
||||
LoginRequest,
|
||||
SignupStartRequest,
|
||||
SignupStartResponse,
|
||||
SignupFinishRequest,
|
||||
LoginResponse,
|
||||
LoginResponseVariants,
|
||||
TenantSnippet,
|
||||
TenantSelectionResponse,
|
||||
TenantSelectionRequest,
|
||||
TenantListResponse,
|
||||
crate::auth::AuthenticatedUser,
|
||||
crate::auth::passkeys::RegistrationChallengeResponse,
|
||||
crate::auth::passkeys::AuthenticationChallengeResponse,
|
||||
crate::auth::passkeys::PasskeySummary,
|
||||
crate::auth::passkeys::PasskeyRegistrationFinishPayload,
|
||||
crate::auth::passkeys::PasskeyLoginStartPayload,
|
||||
crate::auth::passkeys::PasskeyLoginFinishPayload,
|
||||
))
|
||||
)]
|
||||
pub struct AuthApiDoc;
|
||||
|
||||
#[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 async fn login(_state: State<AppState>, _payload: Json<LoginRequest>) -> AppResult<Response> {
|
||||
Err(AppError::bad_request(
|
||||
"password authentication is no longer supported",
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/signup/start",
|
||||
request_body = SignupStartRequest,
|
||||
responses(
|
||||
(status = 200, description = "Signup challenge created", body = SignupStartResponse),
|
||||
(status = 400, description = "Invalid signup request"),
|
||||
(status = 409, description = "Username already exists")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn signup_start(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SignupStartRequest>,
|
||||
@@ -147,6 +206,17 @@ pub async fn signup_start(
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/signup/finish",
|
||||
request_body = SignupFinishRequest,
|
||||
responses(
|
||||
(status = 200, description = "Signup completed", body = LoginResponseVariants),
|
||||
(status = 400, description = "Invalid signup completion"),
|
||||
(status = 409, description = "Username already exists")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn signup_finish(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SignupFinishRequest>,
|
||||
@@ -204,6 +274,15 @@ pub async fn signup_finish(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/refresh",
|
||||
responses(
|
||||
(status = 200, description = "Refreshed access token", body = LoginResponse),
|
||||
(status = 401, description = "Missing or invalid refresh token")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn refresh(
|
||||
State(state): State<AppState>,
|
||||
jar: Option<TypedHeader<Cookie>>,
|
||||
@@ -257,6 +336,13 @@ fn insert_user(conn: &mut PgConnection, id: Uuid, username: &str) -> AppResult<(
|
||||
.map_err(AppError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/select-tenant",
|
||||
request_body = TenantSelectionRequest,
|
||||
responses((status = 200, description = "Tenant selected", body = LoginResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn select_tenant(
|
||||
State(state): State<AppState>,
|
||||
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
|
||||
@@ -293,6 +379,12 @@ pub async fn select_tenant(
|
||||
issue_session(&state, &mut conn, &user, payload.tenant_id)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/logout",
|
||||
responses((status = 204, description = "Session revoked")),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn logout(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
@@ -338,10 +430,22 @@ pub async fn logout(
|
||||
Ok((headers, StatusCode::NO_CONTENT))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/me",
|
||||
responses((status = 200, description = "Authenticated principal", body = AuthenticatedUser)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn me(user: AuthenticatedUser) -> Json<AuthenticatedUser> {
|
||||
Json(user)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/tenants",
|
||||
responses((status = 200, description = "Available tenants", body = TenantListResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn list_tenants(
|
||||
State(state): State<AppState>,
|
||||
auth: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
@@ -374,6 +478,12 @@ pub async fn list_tenants(
|
||||
Ok(Json(TenantListResponse { tenants }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/register/start",
|
||||
responses((status = 200, description = "Passkey registration challenge", body = RegistrationChallengeResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn passkey_register_start(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
@@ -389,6 +499,13 @@ pub async fn passkey_register_start(
|
||||
Ok(Json(challenge))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/register/finish",
|
||||
request_body = PasskeyRegistrationFinishPayload,
|
||||
responses((status = 200, description = "Passkey registered", body = PasskeySummary)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn passkey_register_finish(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
@@ -419,6 +536,13 @@ pub async fn passkey_register_finish(
|
||||
Ok(Json(PasskeySummary::from(passkey)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/login/start",
|
||||
request_body = PasskeyLoginStartPayload,
|
||||
responses((status = 200, description = "Passkey authentication challenge", body = AuthenticationChallengeResponse)),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn passkey_login_start(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<PasskeyLoginStartPayload>,
|
||||
@@ -442,6 +566,16 @@ pub async fn passkey_login_start(
|
||||
Ok(Json(challenge))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/passkeys/login/finish",
|
||||
request_body = PasskeyLoginFinishPayload,
|
||||
responses(
|
||||
(status = 200, description = "Passkey login successful", body = LoginResponseVariants),
|
||||
(status = 401, description = "Authentication failed")
|
||||
),
|
||||
tag = "Auth"
|
||||
)]
|
||||
pub async fn passkey_login_finish(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<PasskeyLoginFinishPayload>,
|
||||
|
||||
@@ -58,6 +58,12 @@ struct CorrespondentChangeset<'a> {
|
||||
metadata: Option<&'a Value>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/correspondents",
|
||||
responses((status = 200, description = "Correspondents", body = [CorrespondentSummary])),
|
||||
tag = "Correspondents"
|
||||
)]
|
||||
pub async fn list_correspondents(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -90,6 +96,13 @@ pub async fn list_correspondents(
|
||||
response.into_json()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/correspondents",
|
||||
request_body = CreateCorrespondentRequest,
|
||||
responses((status = 200, description = "Correspondent created", body = CorrespondentSummary)),
|
||||
tag = "Correspondents"
|
||||
)]
|
||||
pub async fn create_correspondent(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -132,6 +145,14 @@ pub async fn create_correspondent(
|
||||
build_summary(correspondent, 0).into_json()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/correspondents/{id}",
|
||||
params(("id" = Uuid, Path, description = "Correspondent ID")),
|
||||
request_body = UpdateCorrespondentRequest,
|
||||
responses((status = 200, description = "Correspondent updated", body = CorrespondentSummary)),
|
||||
tag = "Correspondents"
|
||||
)]
|
||||
pub async fn update_correspondent(
|
||||
Path(correspondent_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -206,6 +227,13 @@ pub async fn update_correspondent(
|
||||
build_summary(updated, usage).into_json()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/correspondents/{id}",
|
||||
params(("id" = Uuid, Path, description = "Correspondent ID")),
|
||||
responses((status = 204, description = "Correspondent deleted")),
|
||||
tag = "Correspondents"
|
||||
)]
|
||||
pub async fn delete_correspondent(
|
||||
Path(correspondent_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -269,3 +297,20 @@ fn load_usage_for_correspondent(
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::routes::correspondents::list_correspondents,
|
||||
crate::routes::correspondents::create_correspondent,
|
||||
crate::routes::correspondents::update_correspondent,
|
||||
crate::routes::correspondents::delete_correspondent
|
||||
),
|
||||
components(schemas(
|
||||
crate::routes::correspondents::CorrespondentSummary,
|
||||
crate::routes::correspondents::CorrespondentUsage,
|
||||
crate::routes::correspondents::CreateCorrespondentRequest,
|
||||
crate::routes::correspondents::UpdateCorrespondentRequest
|
||||
))
|
||||
)]
|
||||
pub struct CorrespondentsApiDoc;
|
||||
|
||||
@@ -26,6 +26,12 @@ pub struct UpdateDocumentTypeRequest {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/document-types",
|
||||
responses((status = 200, description = "Document types", body = [DocumentTypeResponse])),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub async fn list_document_types(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -43,6 +49,13 @@ pub async fn list_document_types(
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/document-types",
|
||||
request_body = CreateDocumentTypeRequest,
|
||||
responses((status = 201, description = "Document type created", body = DocumentTypeResponse)),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub async fn create_document_type(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -81,6 +94,14 @@ pub async fn create_document_type(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/document-types/{id}",
|
||||
params(("id" = Uuid, Path, description = "Document type ID")),
|
||||
request_body = UpdateDocumentTypeRequest,
|
||||
responses((status = 200, description = "Document type updated", body = DocumentTypeResponse)),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub async fn update_document_type(
|
||||
Path(document_type_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -119,6 +140,13 @@ pub async fn update_document_type(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/document-types/{id}",
|
||||
params(("id" = Uuid, Path, description = "Document type ID")),
|
||||
responses((status = 204, description = "Document type deleted")),
|
||||
tag = "DocumentTypes"
|
||||
)]
|
||||
pub async fn delete_document_type(
|
||||
Path(document_type_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -140,3 +168,19 @@ pub async fn delete_document_type(
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::routes::document_types::list_document_types,
|
||||
crate::routes::document_types::create_document_type,
|
||||
crate::routes::document_types::update_document_type,
|
||||
crate::routes::document_types::delete_document_type
|
||||
),
|
||||
components(schemas(
|
||||
crate::routes::document_types::CreateDocumentTypeRequest,
|
||||
crate::routes::document_types::UpdateDocumentTypeRequest,
|
||||
crate::routes::documents::DocumentTypeResponse
|
||||
))
|
||||
)]
|
||||
pub struct DocumentTypesApiDoc;
|
||||
|
||||
+288
-38
@@ -378,6 +378,13 @@ pub struct AssetObjectsQuery {
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents",
|
||||
params(DocumentListQuery),
|
||||
responses((status = 200, description = "List documents", body = [DocumentResponse])),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn list_documents(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<DocumentListQuery>,
|
||||
@@ -633,48 +640,18 @@ pub async fn list_documents(
|
||||
.load(&mut conn)?
|
||||
};
|
||||
|
||||
let type_ids: HashSet<Uuid> = docs.iter().filter_map(|doc| doc.document_type_id).collect();
|
||||
let doc_type_map: HashMap<Uuid, DocumentType> = if type_ids.is_empty() {
|
||||
HashMap::new()
|
||||
} else {
|
||||
let ids: Vec<Uuid> = type_ids.iter().copied().collect();
|
||||
document_types::table
|
||||
.filter(document_types::tenant_id.eq(tenant_id))
|
||||
.filter(document_types::id.eq_any(&ids))
|
||||
.load::<DocumentType>(&mut conn)?
|
||||
.into_iter()
|
||||
.map(|typ| (typ.id, typ))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let doc_ids: Vec<Uuid> = docs.iter().map(|doc| doc.id).collect();
|
||||
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
|
||||
let mut correspondents_map = load_correspondents_for_documents(&mut conn, &doc_ids)?;
|
||||
drop(conn);
|
||||
|
||||
let primary_versions = load_primary_assets(&state, tenant_id, &docs)?;
|
||||
let mut response = Vec::with_capacity(doc_ids.len());
|
||||
for doc in docs {
|
||||
let tags = tags_map.get(&doc.id).cloned();
|
||||
let correspondents = correspondents_map.remove(&doc.id).unwrap_or_default();
|
||||
let current_version = primary_versions.get(&doc.id).cloned();
|
||||
let doc_type = doc
|
||||
.document_type_id
|
||||
.and_then(|id| doc_type_map.get(&id).cloned());
|
||||
response.push(to_document_response(
|
||||
&state,
|
||||
user_id,
|
||||
doc,
|
||||
tags,
|
||||
correspondents,
|
||||
current_version,
|
||||
doc_type,
|
||||
)?);
|
||||
}
|
||||
let response = hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?;
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents/check",
|
||||
params(DocumentCheckQuery),
|
||||
responses((status = 200, description = "Checksum lookup", body = DocumentCheckResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn check_document(
|
||||
Query(query): Query<DocumentCheckQuery>,
|
||||
TenantScopedConn {
|
||||
@@ -728,6 +705,13 @@ pub async fn check_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 async fn get_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
@@ -773,6 +757,17 @@ pub async fn get_document(
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents",
|
||||
request_body = UploadDocumentForm,
|
||||
responses(
|
||||
(status = 201, description = "Document created", body = DocumentDetailResponse),
|
||||
(status = 200, description = "Existing document reused", body = DocumentDetailResponse),
|
||||
(status = 204, description = "Upload skipped because the document already exists")
|
||||
),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn upload_document(
|
||||
State(state): State<AppState>,
|
||||
TenantScopedConn {
|
||||
@@ -978,6 +973,13 @@ pub async fn upload_document(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/{id}/assets",
|
||||
params(("id" = Uuid, Path, description = "Document ID"), AssetRequestQuery),
|
||||
responses((status = 202, description = "Asset generation requested")),
|
||||
tag = "Assets"
|
||||
)]
|
||||
pub async fn request_document_assets(
|
||||
Path(document_id): Path<Uuid>,
|
||||
Query(query): Query<AssetRequestQuery>,
|
||||
@@ -1014,6 +1016,13 @@ pub async fn request_document_assets(
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/bulk/reanalyze",
|
||||
request_body = BulkReanalyzeSelectionRequest,
|
||||
responses((status = 200, description = "Reanalyze queued", body = BulkReanalyzeResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn reanalyze_selected_documents(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -1065,6 +1074,13 @@ pub async fn reanalyze_selected_documents(
|
||||
Ok((StatusCode::ACCEPTED, Json(BulkReanalyzeResponse { queued })))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents/{id}/assets",
|
||||
params(("id" = Uuid, Path, description = "Document ID")),
|
||||
responses((status = 200, description = "Document assets", body = [DocumentAssetResponse])),
|
||||
tag = "Assets"
|
||||
)]
|
||||
pub async fn list_document_assets(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
@@ -1089,6 +1105,13 @@ pub async fn list_document_assets(
|
||||
Ok(Json(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 = DocumentAssetDetailResponse)),
|
||||
tag = "Assets"
|
||||
)]
|
||||
pub async fn get_document_asset(
|
||||
State(state): State<AppState>,
|
||||
Path(asset_id): Path<Uuid>,
|
||||
@@ -1163,6 +1186,13 @@ pub async fn get_document_asset(
|
||||
Ok(Json(to_asset_detail_response(asset, object_responses)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents/{id}/versions",
|
||||
params(("id" = Uuid, Path, description = "Document ID")),
|
||||
responses((status = 200, description = "Document versions", body = [DocumentVersionResponse])),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn list_document_versions(
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -1192,6 +1222,16 @@ pub async fn list_document_versions(
|
||||
Ok(Json(versions))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/documents/{id}/versions/{version_id}",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "Document ID"),
|
||||
("version_id" = Uuid, Path, description = "Version ID"),
|
||||
),
|
||||
responses((status = 200, description = "Document version detail", body = DocumentVersionDetailResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn get_document_version(
|
||||
State(state): State<AppState>,
|
||||
Path((document_id, version_id)): Path<(Uuid, Uuid)>,
|
||||
@@ -1230,6 +1270,13 @@ pub async fn get_document_version(
|
||||
}))
|
||||
}
|
||||
|
||||
#[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 async fn download_with_token(
|
||||
State(state): State<AppState>,
|
||||
Path(token): Path<String>,
|
||||
@@ -1282,6 +1329,13 @@ pub async fn download_with_token(
|
||||
Ok(axum::response::Redirect::temporary(&presigned_url))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/documents/{id}",
|
||||
params(("id" = Uuid, Path, description = "Document ID")),
|
||||
responses((status = 204, description = "Document deleted")),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn delete_document(
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -1304,6 +1358,14 @@ pub async fn delete_document(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[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 async fn update_document(
|
||||
State(state): State<AppState>,
|
||||
Path(document_id): Path<Uuid>,
|
||||
@@ -1519,6 +1581,14 @@ pub async fn update_document(
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/{id}/restore",
|
||||
params(("id" = Uuid, Path, description = "Document ID")),
|
||||
request_body = RestoreDocumentRequest,
|
||||
responses((status = 204, description = "Document restored")),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn restore_document(
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -1569,6 +1639,14 @@ pub async fn restore_document(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[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 async fn move_document(
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -1597,6 +1675,13 @@ pub async fn move_document(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/bulk/move",
|
||||
request_body = BulkMoveRequest,
|
||||
responses((status = 200, description = "Bulk move outcome", body = BulkMoveResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn bulk_move_documents(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -1680,6 +1765,14 @@ pub async fn bulk_move_documents(
|
||||
Ok((StatusCode::OK, body.into_json()?))
|
||||
}
|
||||
|
||||
#[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 async fn assign_correspondents(
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -1761,6 +1854,13 @@ pub async fn assign_correspondents(
|
||||
no_content()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/bulk/correspondents",
|
||||
request_body = BulkCorrespondentsRequest,
|
||||
responses((status = 200, description = "Bulk correspondents outcome", body = BulkCorrespondentResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn bulk_assign_correspondents(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -1853,6 +1953,16 @@ pub async fn bulk_assign_correspondents(
|
||||
Ok((StatusCode::OK, body.into_json()?))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/documents/{id}/correspondents/{correspondent_id}",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "Document ID"),
|
||||
("correspondent_id" = Uuid, Path, description = "Correspondent ID")
|
||||
),
|
||||
responses((status = 204, description = "Correspondent removed")),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn remove_correspondent(
|
||||
Path((document_id, correspondent_id)): Path<(Uuid, Uuid)>,
|
||||
TenantScopedConn {
|
||||
@@ -1892,6 +2002,14 @@ pub async fn remove_correspondent(
|
||||
no_content()
|
||||
}
|
||||
|
||||
#[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 async fn assign_tags(
|
||||
Path(document_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -1922,6 +2040,13 @@ pub async fn assign_tags(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/documents/bulk/tags",
|
||||
request_body = BulkTagRequest,
|
||||
responses((status = 200, description = "Bulk tag outcome", body = BulkTagResponse)),
|
||||
tag = "Documents"
|
||||
)]
|
||||
pub async fn bulk_update_tags(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -2007,6 +2132,16 @@ pub async fn bulk_update_tags(
|
||||
Ok((StatusCode::OK, response.into_json()?))
|
||||
}
|
||||
|
||||
#[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 async fn remove_tag(
|
||||
Path((document_id, tag_id)): Path<(Uuid, Uuid)>,
|
||||
TenantScopedConn {
|
||||
@@ -2363,3 +2498,118 @@ fn load_document_type(
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hydrate_documents(
|
||||
state: &AppState,
|
||||
conn: &mut PgConnection,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
docs: Vec<Document>,
|
||||
) -> AppResult<Vec<DocumentResponse>> {
|
||||
if docs.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let type_ids: HashSet<Uuid> = docs.iter().filter_map(|doc| doc.document_type_id).collect();
|
||||
let doc_type_map: HashMap<Uuid, DocumentType> = if type_ids.is_empty() {
|
||||
HashMap::new()
|
||||
} else {
|
||||
let ids: Vec<Uuid> = type_ids.iter().copied().collect();
|
||||
document_types::table
|
||||
.filter(document_types::tenant_id.eq(tenant_id))
|
||||
.filter(document_types::id.eq_any(&ids))
|
||||
.load::<DocumentType>(conn)?
|
||||
.into_iter()
|
||||
.map(|typ| (typ.id, typ))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let doc_ids: Vec<Uuid> = docs.iter().map(|doc| doc.id).collect();
|
||||
let tags_map = load_tags_for_documents(conn, &doc_ids)?;
|
||||
let mut correspondents_map = load_correspondents_for_documents(conn, &doc_ids)?;
|
||||
|
||||
let primary_versions = load_primary_assets(state, tenant_id, &docs)?;
|
||||
let mut responses = Vec::with_capacity(doc_ids.len());
|
||||
for doc in docs {
|
||||
let tags = tags_map.get(&doc.id).cloned();
|
||||
let correspondents = correspondents_map.remove(&doc.id).unwrap_or_default();
|
||||
let current_version = primary_versions.get(&doc.id).cloned();
|
||||
let doc_type = doc
|
||||
.document_type_id
|
||||
.and_then(|id| doc_type_map.get(&id).cloned());
|
||||
responses.push(to_document_response(
|
||||
state,
|
||||
user_id,
|
||||
doc,
|
||||
tags,
|
||||
correspondents,
|
||||
current_version,
|
||||
doc_type,
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(responses)
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::routes::documents::list_documents,
|
||||
crate::routes::documents::check_document,
|
||||
crate::routes::documents::upload_document,
|
||||
crate::routes::documents::get_document,
|
||||
crate::routes::documents::update_document,
|
||||
crate::routes::documents::delete_document,
|
||||
crate::routes::documents::restore_document,
|
||||
crate::routes::documents::download_with_token,
|
||||
crate::routes::documents::move_document,
|
||||
crate::routes::documents::assign_tags,
|
||||
crate::routes::documents::remove_tag,
|
||||
crate::routes::documents::bulk_move_documents,
|
||||
crate::routes::documents::bulk_update_tags,
|
||||
crate::routes::documents::bulk_assign_correspondents,
|
||||
crate::routes::documents::assign_correspondents,
|
||||
crate::routes::documents::remove_correspondent,
|
||||
crate::routes::documents::reanalyze_selected_documents,
|
||||
crate::routes::documents::list_document_assets,
|
||||
crate::routes::documents::request_document_assets,
|
||||
crate::routes::documents::get_document_asset,
|
||||
crate::routes::documents::list_document_versions,
|
||||
crate::routes::documents::get_document_version,
|
||||
),
|
||||
components(schemas(
|
||||
crate::routes::documents::DocumentListQuery,
|
||||
crate::routes::documents::DocumentStatusFilter,
|
||||
crate::routes::documents::AssetRequestQuery,
|
||||
crate::routes::documents::DocumentCheckQuery,
|
||||
crate::routes::documents::DocumentCheckResponse,
|
||||
crate::routes::documents::DocumentResponse,
|
||||
crate::routes::documents::DocumentDetailResponse,
|
||||
crate::routes::documents::DocumentMetadataUpdate,
|
||||
crate::routes::documents::DocumentTypeResponse,
|
||||
crate::routes::documents::TagResponse,
|
||||
crate::routes::documents::CorrespondentAssignmentInput,
|
||||
crate::routes::documents::AssignCorrespondentsRequest,
|
||||
crate::routes::documents::BulkCorrespondentAction,
|
||||
crate::routes::documents::BulkCorrespondentsRequest,
|
||||
crate::routes::documents::BulkCorrespondentResponse,
|
||||
crate::routes::documents::BulkMoveRequest,
|
||||
crate::routes::documents::BulkMoveResponse,
|
||||
crate::routes::documents::BulkTagAction,
|
||||
crate::routes::documents::BulkTagRequest,
|
||||
crate::routes::documents::BulkTagResponse,
|
||||
crate::routes::documents::AssignTagsRequest,
|
||||
crate::routes::documents::MoveDocumentRequest,
|
||||
crate::routes::documents::BulkReanalyzeSelectionRequest,
|
||||
crate::routes::documents::BulkReanalyzeResponse,
|
||||
crate::routes::documents::AssetObjectsQuery,
|
||||
crate::routes::documents::UploadDocumentForm,
|
||||
crate::documents::asset::DocumentVersionResponse,
|
||||
crate::documents::asset::DocumentVersionDetailResponse,
|
||||
crate::documents::asset::DocumentAssetResponse,
|
||||
crate::documents::asset::DocumentAssetDetailResponse,
|
||||
crate::documents::asset::DocumentAssetObjectResponse,
|
||||
crate::documents::correspondents::DocumentCorrespondentResponse,
|
||||
))
|
||||
)]
|
||||
pub struct DocumentsApiDoc;
|
||||
|
||||
+100
-54
@@ -1,5 +1,3 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use axum::{
|
||||
extract::{Json, Path, Query, State},
|
||||
http::StatusCode,
|
||||
@@ -9,20 +7,16 @@ use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{Document, DocumentType, Folder, NewFolder};
|
||||
use crate::schema::{document_types, documents, folders};
|
||||
use crate::models::{Document, Folder, NewFolder};
|
||||
use crate::schema::{documents, folders};
|
||||
use crate::state::AppState;
|
||||
use crate::{
|
||||
auth::TenantScopedConn,
|
||||
error::{AppError, AppResult},
|
||||
};
|
||||
|
||||
use super::documents::{to_document_response, DocumentResponse};
|
||||
use crate::documents::{
|
||||
asset::load_primary_assets, correspondents::load_correspondents_for_documents,
|
||||
tags::load_tags_for_documents,
|
||||
};
|
||||
use crate::utils::time::to_iso;
|
||||
use super::documents::{hydrate_documents, DocumentResponse};
|
||||
use crate::utils::{json::deserialize_patch_field, time::to_iso};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateFolderRequest {
|
||||
@@ -74,16 +68,47 @@ pub struct FolderInfo {
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, ToSchema)]
|
||||
#[serde(default)]
|
||||
pub struct UpdateFolderRequest {
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "deserialize_patch_field")]
|
||||
#[schema(nullable, value_type = Option<Uuid>)]
|
||||
pub parent_id: Option<Option<Uuid>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "deserialize_patch_field")]
|
||||
#[schema(nullable)]
|
||||
pub name: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn update_folder_request_deserializes_null_parent() {
|
||||
let req: UpdateFolderRequest =
|
||||
serde_json::from_value(json!({ "parent_id": null })).unwrap();
|
||||
assert!(matches!(req.parent_id, Some(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_folder_request_deserializes_absent_parent() {
|
||||
let req: UpdateFolderRequest = serde_json::from_value(json!({})).unwrap();
|
||||
assert!(req.parent_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_folder_request_deserializes_null_name() {
|
||||
let req: UpdateFolderRequest = serde_json::from_value(json!({ "name": null })).unwrap();
|
||||
assert!(matches!(req.name, Some(None)));
|
||||
}
|
||||
}
|
||||
|
||||
#[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 async fn get_folder(
|
||||
Path(folder_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -102,6 +127,13 @@ pub async fn get_folder(
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/folders/path",
|
||||
request_body = EnsureFolderPathRequest,
|
||||
responses((status = 200, description = "Folder path ensured", body = FolderResponse)),
|
||||
tag = "Folders"
|
||||
)]
|
||||
pub async fn ensure_folder_path(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -188,6 +220,16 @@ pub async fn ensure_folder_path(
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/folders",
|
||||
request_body = CreateFolderRequest,
|
||||
responses(
|
||||
(status = 201, description = "Folder created", body = FolderResponse),
|
||||
(status = 200, description = "Folder already existed", body = FolderResponse)
|
||||
),
|
||||
tag = "Folders"
|
||||
)]
|
||||
pub async fn create_folder(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -275,6 +317,13 @@ pub async fn create_folder(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/folders/{id}/contents",
|
||||
params(("id" = Uuid, Path, description = "Folder ID"), FolderContentsQuery),
|
||||
responses((status = 200, description = "Folder contents", body = FolderContentsResponse)),
|
||||
tag = "Folders"
|
||||
)]
|
||||
pub async fn list_folder_contents(
|
||||
State(state): State<AppState>,
|
||||
Path(folder_identifier): Path<String>,
|
||||
@@ -336,47 +385,7 @@ pub async fn list_folder_contents(
|
||||
.load(&mut conn)?
|
||||
};
|
||||
|
||||
let type_ids: HashSet<Uuid> = docs.iter().filter_map(|doc| doc.document_type_id).collect();
|
||||
let doc_type_map: HashMap<Uuid, DocumentType> = if type_ids.is_empty() {
|
||||
HashMap::new()
|
||||
} else {
|
||||
let ids: Vec<Uuid> = type_ids.iter().copied().collect();
|
||||
document_types::table
|
||||
.filter(document_types::tenant_id.eq(tenant_id))
|
||||
.filter(document_types::id.eq_any(&ids))
|
||||
.load::<DocumentType>(&mut conn)?
|
||||
.into_iter()
|
||||
.map(|typ| (typ.id, typ))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let doc_ids: Vec<Uuid> = docs.iter().map(|doc| doc.id).collect();
|
||||
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
|
||||
let mut correspondents_map = load_correspondents_for_documents(&mut conn, &doc_ids)?;
|
||||
drop(conn);
|
||||
|
||||
let primary_versions = load_primary_assets(&state, tenant_id, &docs)?;
|
||||
|
||||
let mut documents = Vec::with_capacity(doc_ids.len());
|
||||
for doc in docs {
|
||||
let tags = tags_map.get(&doc.id).cloned();
|
||||
let correspondents = correspondents_map.remove(&doc.id).unwrap_or_default();
|
||||
let current_version = primary_versions.get(&doc.id).cloned();
|
||||
let doc_type = doc
|
||||
.document_type_id
|
||||
.and_then(|id| doc_type_map.get(&id).cloned());
|
||||
documents.push(to_document_response(
|
||||
&state,
|
||||
user_id,
|
||||
doc,
|
||||
tags,
|
||||
correspondents,
|
||||
current_version,
|
||||
doc_type,
|
||||
)?);
|
||||
}
|
||||
|
||||
documents
|
||||
hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -388,6 +397,13 @@ pub async 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 async fn delete_folder(
|
||||
Path(folder_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -442,6 +458,14 @@ pub async fn delete_folder(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[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 async fn update_folder(
|
||||
Path(folder_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -586,3 +610,25 @@ pub(super) fn gather_descendant_folder_ids(
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::routes::folders::create_folder,
|
||||
crate::routes::folders::ensure_folder_path,
|
||||
crate::routes::folders::get_folder,
|
||||
crate::routes::folders::list_folder_contents,
|
||||
crate::routes::folders::delete_folder,
|
||||
crate::routes::folders::update_folder
|
||||
),
|
||||
components(schemas(
|
||||
crate::routes::folders::CreateFolderRequest,
|
||||
crate::routes::folders::EnsureFolderPathRequest,
|
||||
crate::routes::folders::FolderResponse,
|
||||
crate::routes::folders::FolderInfo,
|
||||
crate::routes::folders::FolderContentsQuery,
|
||||
crate::routes::folders::FolderContentsResponse,
|
||||
crate::routes::folders::UpdateFolderRequest
|
||||
))
|
||||
)]
|
||||
pub struct FoldersApiDoc;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
use axum::{http::StatusCode, response::Json};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(paths(crate::routes::health::health_check))]
|
||||
pub struct HealthApiDoc;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/health",
|
||||
responses((status = 200, description = "Service is healthy")),
|
||||
tag = "Health"
|
||||
)]
|
||||
pub async fn health_check() -> (StatusCode, Json<serde_json::Value>) {
|
||||
(StatusCode::OK, Json(json!({ "status": "ok" })))
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ pub struct RevokePasskeyQuery {
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/profile/passkeys",
|
||||
responses((status = 200, description = "List registered passkeys", body = [PasskeySummary])),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub async fn list_passkeys(
|
||||
State(state): State<AppState>,
|
||||
TenantScopedConn {
|
||||
@@ -72,6 +78,12 @@ pub async fn list_passkeys(
|
||||
Ok(Json(passkeys))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/profile/webdav-tokens",
|
||||
responses((status = 200, description = "List WebDAV tokens", body = [WebdavTokenResponse])),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub async fn list_webdav_tokens(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -85,6 +97,13 @@ pub async fn list_webdav_tokens(
|
||||
Ok(Json(responses))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/profile/webdav-tokens",
|
||||
request_body = CreateWebdavTokenRequest,
|
||||
responses((status = 201, description = "WebDAV token created", body = WebdavTokenCreatedResponse)),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub async fn create_webdav_token(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -115,6 +134,13 @@ pub async fn create_webdav_token(
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/profile/webdav-tokens/{id}",
|
||||
params(("id" = Uuid, Path, description = "WebDAV token ID")),
|
||||
responses((status = 204, description = "WebDAV token revoked")),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub async fn delete_webdav_token(
|
||||
TenantScopedConn {
|
||||
mut conn, user_id, ..
|
||||
@@ -125,6 +151,16 @@ pub async fn delete_webdav_token(
|
||||
no_content()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/profile/passkeys/{id}",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "Passkey ID"),
|
||||
("reason" = Option<String>, Query, description = "Optional reason for revoking the passkey")
|
||||
),
|
||||
responses((status = 204, description = "Passkey revoked")),
|
||||
tag = "Profile"
|
||||
)]
|
||||
pub async fn delete_passkey(
|
||||
State(state): State<AppState>,
|
||||
TenantScopedConn {
|
||||
@@ -166,3 +202,22 @@ fn parse_timestamp(value: &str) -> AppResult<NaiveDateTime> {
|
||||
.map_err(|_| AppError::bad_request("invalid expires_at timestamp"))?;
|
||||
Ok(dt.naive_utc())
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::routes::profile::list_webdav_tokens,
|
||||
crate::routes::profile::create_webdav_token,
|
||||
crate::routes::profile::delete_webdav_token,
|
||||
crate::routes::profile::list_passkeys,
|
||||
crate::routes::profile::delete_passkey
|
||||
),
|
||||
components(schemas(
|
||||
crate::routes::profile::WebdavTokenResponse,
|
||||
crate::routes::profile::WebdavTokenCreatedResponse,
|
||||
crate::routes::profile::CreateWebdavTokenRequest,
|
||||
crate::routes::profile::RevokePasskeyQuery,
|
||||
crate::auth::passkeys::PasskeySummary
|
||||
))
|
||||
)]
|
||||
pub struct ProfileApiDoc;
|
||||
|
||||
@@ -9,7 +9,10 @@ use crate::auth::TenantScopedConn;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::models::{NewTag, Tag};
|
||||
use crate::schema::{document_tags, tags};
|
||||
use crate::utils::db::{no_content, EnsureEntity, IntoJsonResponse};
|
||||
use crate::utils::{
|
||||
db::{no_content, EnsureEntity, IntoJsonResponse},
|
||||
json::deserialize_patch_field,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateTagRequest {
|
||||
@@ -25,6 +28,30 @@ struct UpdateTagChangeset<'a> {
|
||||
color: Option<Option<&'a str>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn update_tag_request_deserializes_null_fields() {
|
||||
let request: UpdateTagRequest = serde_json::from_value(json!({
|
||||
"label": null,
|
||||
"color": null
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(matches!(request.label, Some(None)));
|
||||
assert!(matches!(request.color, Some(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_tag_request_omitted_fields_are_none() {
|
||||
let request: UpdateTagRequest = serde_json::from_value(json!({})).unwrap();
|
||||
assert!(request.label.is_none());
|
||||
assert!(request.color.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct TagCatalogEntry {
|
||||
pub id: Uuid,
|
||||
@@ -35,16 +62,21 @@ pub struct TagCatalogEntry {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, ToSchema)]
|
||||
#[serde(default)]
|
||||
pub struct UpdateTagRequest {
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "deserialize_patch_field")]
|
||||
#[schema(nullable, value_type = Option<String>)]
|
||||
pub label: Option<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, deserialize_with = "deserialize_patch_field")]
|
||||
#[schema(nullable, value_type = Option<String>)]
|
||||
pub color: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/tags",
|
||||
responses((status = 200, description = "Tags", body = [TagCatalogEntry])),
|
||||
tag = "Tags"
|
||||
)]
|
||||
pub async fn list_tags(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -78,6 +110,13 @@ pub async fn list_tags(
|
||||
response.into_json()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/tags",
|
||||
request_body = CreateTagRequest,
|
||||
responses((status = 200, description = "Tag created", body = TagCatalogEntry)),
|
||||
tag = "Tags"
|
||||
)]
|
||||
pub async fn create_tag(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -126,6 +165,14 @@ pub async fn create_tag(
|
||||
.into_json()
|
||||
}
|
||||
|
||||
#[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 async fn update_tag(
|
||||
Path(tag_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -253,6 +300,13 @@ pub async fn update_tag(
|
||||
.into_json()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/tags/{id}",
|
||||
params(("id" = Uuid, Path, description = "Tag ID")),
|
||||
responses((status = 204, description = "Tag deleted")),
|
||||
tag = "Tags"
|
||||
)]
|
||||
pub async fn delete_tag(
|
||||
Path(tag_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -285,3 +339,19 @@ pub async fn delete_tag(
|
||||
|
||||
no_content()
|
||||
}
|
||||
|
||||
#[derive(utoipa::OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::routes::tags::list_tags,
|
||||
crate::routes::tags::create_tag,
|
||||
crate::routes::tags::update_tag,
|
||||
crate::routes::tags::delete_tag
|
||||
),
|
||||
components(schemas(
|
||||
crate::routes::tags::CreateTagRequest,
|
||||
crate::routes::tags::TagCatalogEntry,
|
||||
crate::routes::tags::UpdateTagRequest
|
||||
))
|
||||
)]
|
||||
pub struct TagsApiDoc;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
pub enum NullableValue {
|
||||
@@ -14,3 +15,11 @@ pub fn classify_nullable(optional_value: Option<&Value>) -> Result<NullableValue
|
||||
Some(other) => Err(format!("expected string or null, got {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_patch_field<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
Option::<T>::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
+14
-14
@@ -2,16 +2,16 @@ mod common;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use axum::http::{header::SET_COOKIE, StatusCode};
|
||||
use backend::auth::passkeys::{
|
||||
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
|
||||
RegistrationChallengeResponse,
|
||||
};
|
||||
use backend::models::{NewRefreshToken, NewUserMembership, TenantStatus, UserPasskey};
|
||||
use backend::openapi::schemas::PasskeySummary;
|
||||
use backend::schema::{refresh_tokens, tenants, user_memberships, users};
|
||||
use chrono::{Duration as ChronoDuration, Utc};
|
||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
||||
use diesel::prelude::*;
|
||||
use papercrate::auth::passkeys::{
|
||||
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
|
||||
RegistrationChallengeResponse,
|
||||
};
|
||||
use papercrate::models::{NewRefreshToken, NewUserMembership, TenantStatus, UserPasskey};
|
||||
use papercrate::openapi::schemas::PasskeySummary;
|
||||
use papercrate::schema::{refresh_tokens, tenants, user_memberships, users};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use serde::Deserialize;
|
||||
@@ -169,8 +169,8 @@ async fn passkey_register_start_creates_challenge() -> Result<()> {
|
||||
let challenge_id = challenge.challenge_id;
|
||||
|
||||
app.with_conn(move |conn| {
|
||||
use backend::schema::webauthn_challenges::dsl;
|
||||
use diesel::dsl::{exists, select};
|
||||
use papercrate::schema::webauthn_challenges::dsl;
|
||||
|
||||
let exists: bool = select(exists(
|
||||
dsl::webauthn_challenges.filter(dsl::id.eq(challenge_id)),
|
||||
@@ -322,7 +322,7 @@ async fn delete_passkey_soft_revokes() -> Result<()> {
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
app.with_conn(move |conn| {
|
||||
use backend::schema::user_passkeys::dsl as passkey_dsl;
|
||||
use papercrate::schema::user_passkeys::dsl as passkey_dsl;
|
||||
|
||||
let record = passkey_dsl::user_passkeys
|
||||
.find(passkey_id)
|
||||
@@ -574,18 +574,18 @@ async fn login_with_session(
|
||||
let username = username.to_string();
|
||||
let state = app.state.clone();
|
||||
app.with_conn(move |conn| {
|
||||
use backend::schema::user_memberships::dsl as memberships_dsl;
|
||||
use backend::schema::users::dsl as users_dsl;
|
||||
use papercrate::schema::user_memberships::dsl as memberships_dsl;
|
||||
use papercrate::schema::users::dsl as users_dsl;
|
||||
|
||||
let user: backend::models::User = users_dsl::users
|
||||
let user: papercrate::models::User = users_dsl::users
|
||||
.filter(users_dsl::username.eq(&username))
|
||||
.first(conn)?;
|
||||
|
||||
let membership: backend::models::UserMembership = memberships_dsl::user_memberships
|
||||
let membership: papercrate::models::UserMembership = memberships_dsl::user_memberships
|
||||
.filter(memberships_dsl::user_id.eq(user.id))
|
||||
.first(conn)?;
|
||||
|
||||
let tenant: backend::models::Tenant =
|
||||
let tenant: papercrate::models::Tenant =
|
||||
tenants::table.find(membership.tenant_id).first(conn)?;
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
+27
-25
@@ -8,17 +8,6 @@ use async_trait::async_trait;
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Method, Request};
|
||||
use axum::Router;
|
||||
use backend::auth::jwt::JwtService;
|
||||
use backend::config::AppConfig;
|
||||
use backend::db::{self, PgPool};
|
||||
use backend::models::{
|
||||
Job, NewRefreshToken, NewUser, NewUserMembership, NewUserPasskey, Tenant, TenantStatus, User,
|
||||
UserMembership,
|
||||
};
|
||||
use backend::routes;
|
||||
use backend::schema::refresh_tokens::dsl as refresh_dsl;
|
||||
use backend::state::AppState;
|
||||
use backend::storage::ObjectStorage;
|
||||
use chrono::{Duration as ChronoDuration, Utc};
|
||||
use diesel::connection::SimpleConnection;
|
||||
use diesel::prelude::*;
|
||||
@@ -27,9 +16,20 @@ use diesel::PgConnection;
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use http_body_util::BodyExt;
|
||||
use once_cell::sync::Lazy;
|
||||
use papercrate::auth::jwt::JwtService;
|
||||
use papercrate::config::AppConfig;
|
||||
use papercrate::db::{self, PgPool};
|
||||
use papercrate::models::{
|
||||
Job, NewRefreshToken, NewUser, NewUserMembership, NewUserPasskey, Tenant, TenantStatus, User,
|
||||
UserMembership,
|
||||
};
|
||||
use papercrate::routes;
|
||||
use papercrate::schema::refresh_tokens::dsl as refresh_dsl;
|
||||
use papercrate::state::AppState;
|
||||
use papercrate::storage::ObjectStorage;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use serde_json::{self, json};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::sync::Mutex;
|
||||
@@ -37,8 +37,8 @@ use tower::util::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
const RESET_SCHEMA_SQL: &str =
|
||||
include_str!("../../migrations/202510300000_initial_schema/down.sql");
|
||||
const RESET_DATABASE_SQL: &str =
|
||||
"DROP SCHEMA IF EXISTS public CASCADE;\nCREATE SCHEMA public;\nGRANT ALL ON SCHEMA public TO public;";
|
||||
|
||||
static DB_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
|
||||
@@ -222,7 +222,7 @@ impl TestApp {
|
||||
id: Uuid::new_v4(),
|
||||
username,
|
||||
};
|
||||
diesel::insert_into(backend::schema::users::table)
|
||||
diesel::insert_into(papercrate::schema::users::table)
|
||||
.values(&user)
|
||||
.execute(conn)
|
||||
.context("failed to insert user")?;
|
||||
@@ -233,7 +233,7 @@ impl TestApp {
|
||||
tenant_id,
|
||||
};
|
||||
|
||||
diesel::insert_into(backend::schema::user_memberships::table)
|
||||
diesel::insert_into(papercrate::schema::user_memberships::table)
|
||||
.values(&membership)
|
||||
.execute(conn)
|
||||
.context("failed to insert user membership")?;
|
||||
@@ -260,7 +260,7 @@ impl TestApp {
|
||||
nickname,
|
||||
};
|
||||
|
||||
diesel::insert_into(backend::schema::user_passkeys::table)
|
||||
diesel::insert_into(papercrate::schema::user_passkeys::table)
|
||||
.values(&passkey)
|
||||
.execute(conn)
|
||||
.context("failed to insert passkey")?;
|
||||
@@ -274,7 +274,7 @@ impl TestApp {
|
||||
let name_value = TEST_TENANT_NAME.to_string();
|
||||
let quickwit_enabled = self.state.config.quickwit_endpoint.is_some();
|
||||
self.with_conn(move |conn| {
|
||||
use backend::schema::tenants::dsl as tenants_dsl;
|
||||
use papercrate::schema::tenants::dsl as tenants_dsl;
|
||||
|
||||
let existing = tenants_dsl::tenants
|
||||
.filter(tenants_dsl::name.eq(&name_value))
|
||||
@@ -334,9 +334,9 @@ impl TestApp {
|
||||
let username = username.to_string();
|
||||
let state = self.state.clone();
|
||||
self.with_conn(move |conn| {
|
||||
use backend::schema::tenants::dsl as tenants_dsl;
|
||||
use backend::schema::user_memberships::dsl as memberships_dsl;
|
||||
use backend::schema::users::dsl as users_dsl;
|
||||
use papercrate::schema::tenants::dsl as tenants_dsl;
|
||||
use papercrate::schema::user_memberships::dsl as memberships_dsl;
|
||||
use papercrate::schema::users::dsl as users_dsl;
|
||||
|
||||
let user: User = users_dsl::users
|
||||
.filter(users_dsl::username.eq(&username))
|
||||
@@ -383,7 +383,7 @@ impl TestApp {
|
||||
#[allow(dead_code)]
|
||||
pub async fn clear_jobs(&self) -> Result<()> {
|
||||
self.with_conn(|conn| {
|
||||
use backend::schema::jobs::dsl::jobs as jobs_table;
|
||||
use papercrate::schema::jobs::dsl::jobs as jobs_table;
|
||||
diesel::delete(jobs_table)
|
||||
.execute(conn)
|
||||
.context("failed to clear jobs")?;
|
||||
@@ -396,7 +396,7 @@ impl TestApp {
|
||||
pub async fn jobs_by_type(&self, ty: &str) -> Result<Vec<Job>> {
|
||||
let ty = ty.to_string();
|
||||
self.with_conn(move |conn| {
|
||||
use backend::schema::jobs::dsl::{job_type as job_type_col, jobs as jobs_table};
|
||||
use papercrate::schema::jobs::dsl::{job_type as job_type_col, jobs as jobs_table};
|
||||
let rows = jobs_table
|
||||
.filter(job_type_col.eq(&ty))
|
||||
.load::<Job>(conn)
|
||||
@@ -699,8 +699,10 @@ async fn prepare_database(pool: &PgPool) -> Result<()> {
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|err| anyhow!("failed to acquire connection: {err}"))?;
|
||||
let _ = conn.batch_execute(RESET_SCHEMA_SQL);
|
||||
let _ = conn.batch_execute("DROP TABLE IF EXISTS __diesel_schema_migrations;");
|
||||
conn.batch_execute(RESET_DATABASE_SQL)
|
||||
.map_err(|err| anyhow!("failed to reset schema: {err}"))?;
|
||||
conn.batch_execute("DROP TABLE IF EXISTS __diesel_schema_migrations;")
|
||||
.map_err(|err| anyhow!("failed to drop diesel schema table: {err}"))?;
|
||||
conn.run_pending_migrations(MIGRATIONS)
|
||||
.map_err(|err| anyhow!("failed to run migrations: {err}"))?;
|
||||
truncate_all(&mut conn)?;
|
||||
|
||||
@@ -2,13 +2,13 @@ mod common;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::http::StatusCode;
|
||||
use backend::models::{NewUser, NewUserMembership, Tag, TenantStatus};
|
||||
use backend::schema::{
|
||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
||||
use diesel::prelude::*;
|
||||
use papercrate::models::{NewUser, NewUserMembership, Tag, TenantStatus};
|
||||
use papercrate::schema::{
|
||||
tags::dsl as tags_dsl, tenants::dsl as tenants_dsl, user_memberships::dsl as memberships_dsl,
|
||||
users::dsl as users_dsl,
|
||||
};
|
||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
||||
use diesel::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -3,13 +3,13 @@ mod common;
|
||||
use anyhow::Result;
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Method, Request, StatusCode};
|
||||
use backend::models::WebdavToken;
|
||||
use backend::routes::webdav;
|
||||
use backend::schema::webdav_tokens;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use base64::Engine;
|
||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
||||
use diesel::prelude::*;
|
||||
use papercrate::models::WebdavToken;
|
||||
use papercrate::routes::webdav;
|
||||
use papercrate::schema::webdav_tokens;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tower::ServiceExt;
|
||||
|
||||
Reference in New Issue
Block a user