feat(backend): implement file upload service for dispute evidence (#412)#452
Open
Jaydams wants to merge 1 commit into
Open
feat(backend): implement file upload service for dispute evidence (#412)#452Jaydams wants to merge 1 commit into
Jaydams wants to merge 1 commit into
Conversation
…ayLitCodes#412) Add UploadModule providing a complete evidence file management system for disputes. The module is self-contained and uses abstract adapter interfaces so storage (local → S3) and virus scanning (noop → ClamAV) can be swapped without touching business logic. New endpoints: POST /disputes/:id/evidence upload a file GET /disputes/:id/evidence list evidence GET /disputes/:id/evidence/:evidenceId/download stream download DELETE /disputes/:id/evidence/:evidenceId admin-only soft delete Key implementation details: - Magic-bytes MIME validation (not extension trust) for PNG, JPEG, WebP, PDF, TXT, DOCX - Max 10 MB per file enforced at multer + service layers - Max 10 active files per dispute enforced at service layer - UUID filenames on disk prevent path-traversal attacks - Image thumbnails (200×200 JPEG) via sharp with graceful fallback - LocalStorageAdapter (default) + S3StorageAdapter stub ready to wire - VirusScanner interface with NoopVirusScannerAdapter; replace with ClamAV adapter by binding VIRUS_SCANNER token - UploadRateLimitGuard: 20 uploads / hour / user (in-memory window) - DisputeAccessGuard: checks caller is a party to the dispute's escrow - Admin-only DELETE with soft-delete + physical file removal - cleanOrphanedFiles() utility for scheduled cleanup - Migration 1780400000000-AddDisputeEvidence adds dispute_evidence table with FK cascade and indices on disputeId / uploadedByUserId - 12 unit tests covering all happy paths and error branches - E2E tests against in-memory SQLite covering upload, list, download, access control, and error cases
|
@Jaydams Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #412
Dispute resolution had no mechanism for parties to submit supporting
evidence files. This PR adds a complete, self-contained
UploadModulethat handles everything from ingestion through storage, thumbnail
generation, access control, and deletion.
What Changed
New Module:
apps/backend/src/modules/upload/entities/dispute-evidence.entity.tsdispute_evidencetable — stores all file metadatainterfaces/storage-adapter.interface.tsStorageAdaptercontract (save / stream / delete / exists)interfaces/virus-scanner.interface.tsVirusScannercontract (scan →{clean, threat})adapters/local-storage.adapter.tsadapters/s3-storage.adapter.tsSTORAGE_ADAPTERtoken to swap inadapters/noop-virus-scanner.adapter.tsVIRUS_SCANNERtoken to activateguards/dispute-access.guard.tsguards/upload-rate-limit.guard.tsupload.service.tsupload.controller.ts/disputes/:id/evidenceupload.module.tsupload.service.spec.tsSupporting Changes
src/migrations/1780400000000-AddDisputeEvidence.tsdispute_evidencetable + two indicessrc/app.module.tsUploadModuleandDisputeEvidenceentitypackage.jsonsharp ^0.33.5+@types/sharp ^0.31.1test/e2e/upload.e2e-spec.tsEndpoints
POST /disputes/:id/evidenceAuthGuard→DisputeAccessGuard→UploadRateLimitGuardmemoryStorage, 10 MB hard limitEvidenceResponseDtoGET /disputes/:id/evidenceAuthGuard→DisputeAccessGuard{ data: EvidenceResponseDto[], total: number }ordered bycreatedAt ASCGET /disputes/:id/evidence/:evidenceId/downloadAuthGuard→DisputeAccessGuardContent-Type,Content-Disposition: attachment, andContent-LengthheadersDELETE /disputes/:id/evidence/:evidenceIdAuthGuard→DisputeAccessGuard→AdminGuardisDeleted,deletedByUserId,deletedAt)Security
Content-Typeor file extension. This prevents extension-spoofing attacks.{uuid}{ext}, making the original name irrelevant on disk and eliminating path-traversal via crafted filenames.UploadRateLimitGuard. Keyed on authenticated user ID, not IP.DisputeAccessGuardresolves the dispute → escrow link and verifies the caller is a party. Outsiders receive 403.AdminGuardenforcesADMINorSUPER_ADMINrole on the DELETE route, and the service double-checksisUserAdmin()before proceeding.VirusScannerinterface before being written to storage. The defaultNoopVirusScannerAdapteris a safe placeholder; replace with a ClamAV adapter by re-binding theVIRUS_SCANNERtoken inUploadModule.Storage & Thumbnail
Local storage (default): files saved to
./uploads/evidence/{uuid}{ext}. Thumbnails saved to./uploads/thumbnails/{uuid}_thumb.jpg.S3 (future): implement
StorageAdapterand provide via: