feat(storage): wire MinIO/S3 document upload & download into the API + web
The schema has carried `storageKey` pointers and the migration has written blobs to MinIO since day one, but the API had no S3 client — documents could only be deleted, never uploaded or retrieved. This adds the missing wiring. API - StorageModule/StorageService (@aws-sdk/client-s3, path-style for MinIO): put/getStream/delete, best-effort bucket ensure on boot, gracefully disabled when S3 env is absent (ServiceUnavailable on use). - Reads S3_ENDPOINT/S3_BUCKET + S3_ACCESS_KEY/S3_SECRET_KEY, falling back to MINIO_ROOT_USER/MINIO_ROOT_PASSWORD so one credential set drives both the migration and the API. - Property service documents: POST :id/documents (multipart), GET :id/documents/:childId/download (streamed), delete now also drops the blob. - Policy documents: same upload/download/delete (previously had none). - Keys stay under the service/<id>/… and policy/<id>/… prefixes the migration established. Web - api.ts: shared uploadFile() helper (uploadIngest refactored onto it), upload/download/remove helpers for property & policy documents. - Servicios, polizas, clientes detail pages: real Descargar links and an upload control (gated by policy:update / property:update) replacing the "storage pending" notes. Infra - docker-compose: minio service (9000/9001, healthcheck, named volume) + S3 env wired into the api service. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+58
-4
@@ -413,6 +413,47 @@ export function removePropertyDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function propertyDocumentDownloadUrl(
|
||||
propertyId: string,
|
||||
documentId: string,
|
||||
): string {
|
||||
return `${API_ORIGIN}/properties/${propertyId}/documents/${documentId}/download`;
|
||||
}
|
||||
|
||||
export function uploadPropertyDocument(
|
||||
propertyId: string,
|
||||
file: File,
|
||||
type?: string,
|
||||
): Promise<unknown> {
|
||||
const q = type ? `?type=${encodeURIComponent(type)}` : "";
|
||||
return uploadFile(`/properties/${propertyId}/documents${q}`, file);
|
||||
}
|
||||
|
||||
export function removePolicyDocument(
|
||||
policyId: string,
|
||||
documentId: string,
|
||||
): Promise<unknown> {
|
||||
return apiFetch(`/policies/${policyId}/documents/${documentId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export function policyDocumentDownloadUrl(
|
||||
policyId: string,
|
||||
documentId: string,
|
||||
): string {
|
||||
return `${API_ORIGIN}/policies/${policyId}/documents/${documentId}/download`;
|
||||
}
|
||||
|
||||
export function uploadPolicyDocument(
|
||||
policyId: string,
|
||||
file: File,
|
||||
type?: string,
|
||||
): Promise<unknown> {
|
||||
const q = type ? `?type=${encodeURIComponent(type)}` : "";
|
||||
return uploadFile(`/policies/${policyId}/documents${q}`, file);
|
||||
}
|
||||
|
||||
/* ------------------------------------------- Billing / statements module */
|
||||
|
||||
export interface MovementQuery {
|
||||
@@ -604,11 +645,19 @@ export function listIngest(): Promise<IngestFile[]> {
|
||||
return apiFetch<IngestFile[]>("/ops/ingest");
|
||||
}
|
||||
|
||||
/** Multipart upload — not JSON, so it bypasses apiFetch's Content-Type. */
|
||||
export async function uploadIngest(name: string, file: File): Promise<void> {
|
||||
/**
|
||||
* Multipart upload — not JSON, so it bypasses apiFetch's Content-Type. `path`
|
||||
* is API-relative (may include a query string); `filename` overrides the part
|
||||
* name sent to the server.
|
||||
*/
|
||||
export async function uploadFile(
|
||||
path: string,
|
||||
file: File,
|
||||
filename?: string,
|
||||
): Promise<unknown> {
|
||||
const body = new FormData();
|
||||
body.append("file", file, name);
|
||||
const res = await fetch(`${API_ORIGIN}/ops/ingest/${encodeURIComponent(name)}`, {
|
||||
body.append("file", file, filename ?? file.name);
|
||||
const res = await fetch(`${API_ORIGIN}${path}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body,
|
||||
@@ -623,6 +672,11 @@ export async function uploadIngest(name: string, file: File): Promise<void> {
|
||||
}
|
||||
throw new ApiError(res.status, message);
|
||||
}
|
||||
return res.status === 204 ? undefined : res.json().catch(() => undefined);
|
||||
}
|
||||
|
||||
export function uploadIngest(name: string, file: File): Promise<unknown> {
|
||||
return uploadFile(`/ops/ingest/${encodeURIComponent(name)}`, file, name);
|
||||
}
|
||||
|
||||
export function deleteIngest(name: string): Promise<unknown> {
|
||||
|
||||
Reference in New Issue
Block a user