VPR-59 [1/6] CMS migration: file management (API + UI)#245
Conversation
- New /api/cms/files endpoints: paged list with filters, get, folders, multipart upload, edit with permission/person deltas, encrypt toggle, file replacement, soft delete/restore, admin permanent delete, and a queryable fileAudit log matching legacy audit actions - CmsFileCrypto implements ColdFusion-compatible AES-128-ECB with UU-encoded per-file keys so both systems can read each other's encrypted files during coexistence; CMS.cs decrypt now delegates to it - CmsFileStorageService validates folders against the storage root (containment + top-level whitelist), uploads via temp-then-move, and reproduces legacy _0.._999 collision renaming - Encrypt/decrypt-in-place writes are atomic (temp file + move) to avoid corrupting files on interrupted writes
- Files page at /CMS/ManageFiles (not /CMS/Files, which is the MVC download handler): server-paged list with folder/status/search/ encrypted filters, upload and edit dialogs, soft delete/restore - Shared PermissionSelector and PersonSelector backed by new /api/cms/options endpoints, since the RAPS permission list requires RAPS roles + 2FA and Directory search is too heavy for type-ahead - File audit log page with action/user/date/search filters and per-file filtering via ?fileGuid= - CMS home cards shown per user permission - ViperFetch gains postForm/putForm for multipart uploads
Bundle ReportChanges will increase total bundle size by 21.74kB (1.02%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: viper-frontend-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #245 +/- ##
==========================================
+ Coverage 44.65% 44.94% +0.29%
==========================================
Files 897 908 +11
Lines 51890 52686 +796
Branches 4868 4961 +93
==========================================
+ Hits 23169 23681 +512
- Misses 28141 28387 +246
- Partials 580 618 +38
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
|
|
||
| private static IFormFile MakeFormFile(string fileName = "report.pdf") | ||
| { | ||
| var stream = new MemoryStream(new byte[] { 1, 2, 3 }); |
There was a problem hiding this comment.
Fixed: the test helper now tracks each FormFile's backing stream and the fixture disposes them. Carried into the restacked stack (#251).
|
|
||
| private static IFormFile MakeFormFile(string fileName = "report.pdf") | ||
| { | ||
| var stream = new MemoryStream(new byte[] { 1, 2, 3 }); |
There was a problem hiding this comment.
Fixed together with the code-quality finding above — streams are tracked and disposed by the fixture. Carried into #251.
There was a problem hiding this comment.
Pull request overview
Implements the first slice of the CMS migration focused on file management, adding a new CMS files management API and Vue SPA UI while keeping the legacy download handler intact for existing URL shapes.
Changes:
- Added CMS backend services for file storage, CRUD orchestration, auditing, and ColdFusion-compatible encryption primitives.
- Added CMS file-management API endpoints (
/api/cms/files,/api/cms/options) with paging/filtering, upload/edit/delete/restore, and audit-log access. - Added Vue CMS pages/components for managing files, uploading/editing metadata, and viewing the audit log, plus multipart
useFetchhelpers.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web/Areas/CMS/Services/CmsFileStorageService.cs | New storage-root–scoped file IO, folder validation, and collision handling. |
| web/Areas/CMS/Services/CmsFileService.cs | New CRUD/query orchestration over CMS files, permissions/people deltas, and audit writes. |
| web/Areas/CMS/Services/CmsFileEncryptionService.cs | DI wrapper around crypto that resolves master key and rewrites file contents. |
| web/Areas/CMS/Services/CmsFileCrypto.cs | CF-compatible AES-ECB + UUEncode primitives for key/content encryption. |
| web/Areas/CMS/Services/CmsFileAuditService.cs | New audit writer + paged/filterable audit query API. |
| web/Areas/CMS/Models/DTOs/CmsFileRequests.cs | Request DTOs for create/update multipart form fields. |
| web/Areas/CMS/Models/DTOs/CmsFileDto.cs | Response DTOs for file rows and people display names. |
| web/Areas/CMS/Models/CmsFileMapper.cs | Mapperly-based entity→DTO mapping with computed URLs and collections. |
| web/Areas/CMS/Data/CMS.cs | Reuses new crypto primitives for legacy decrypt paths; MIME types moved to CmsFileTypes. |
| web/Areas/CMS/Controllers/CMSOptionsController.cs | New options endpoints for permissions and person typeahead for CMS managers. |
| web/Areas/CMS/Controllers/CMSFilesController.cs | New files management API endpoints (list/upload/edit/delete/restore/audit). |
| web/Areas/CMS/Constants/CmsPermissions.cs | Introduces CMS-related permission constants used by the new API/controllers. |
| web/Areas/CMS/Constants/CmsFileTypes.cs | Central allow-list for extensions→MIME types and filename checks. |
| VueApp/src/composables/ViperFetch.ts | Adds multipart postForm / putForm helpers for file uploads. |
| VueApp/src/CMS/types/index.ts | Adds CMS file/audit/person option TypeScript types. |
| VueApp/src/CMS/router/routes.ts | Adds SPA routes for /CMS/ManageFiles and /CMS/ManageFiles/Audit. |
| VueApp/src/CMS/pages/Files.vue | New server-paged QTable UI for files with filters and actions. |
| VueApp/src/CMS/pages/FileAuditLog.vue | New server-paged QTable UI for audit log with filters. |
| VueApp/src/CMS/pages/CmsHome.vue | Adds CMS home cards including entry points for file management. |
| VueApp/src/CMS/components/PersonSelector.vue | New async person search selector backed by /api/cms/options/people. |
| VueApp/src/CMS/components/PermissionSelector.vue | New permission selector backed by /api/cms/options/permissions. |
| VueApp/src/CMS/components/FileFormDialog.vue | New upload/edit dialog composing selectors and multipart submit. |
| test/CMS/CmsFileStorageServiceTests.cs | Unit tests for storage validation, containment, and collision logic. |
| test/CMS/CmsFileServiceTests.cs | Unit tests for CRUD orchestration, deltas, auditing, and list filters. |
| test/CMS/CmsFileCryptoTests.cs | Unit tests for crypto key/content round-tripping and master-key parsing. |
| if (val.trim().length < 2) { | ||
| update(() => { | ||
| options.value = [] | ||
| }) | ||
| return |
There was a problem hiding this comment.
Fixed in the restack: dropping below 2 chars now bumps the search sequence, so a late in-flight response is discarded instead of repopulating the cleared list. In #253.
| // Also check the DB in case a record exists whose disk file is missing; a new file | ||
| // at that path would be served under the orphaned record's permissions. | ||
| return _context.Files.Any(f => f.FilePath == targetPath); |
There was a problem hiding this comment.
Fixed in the restack: the DB collision check now also matches the folder+name path suffix in either separator style, covering records stored under another environment's storage root (which ReplaceRootFolder rewrites at read time). Regression test added. In #251.
| // Folder may have subfolders stored as "folder\sub"; match the top-level folder. | ||
| var folderPrefix = folder + @"\"; | ||
| query = query.Where(f => f.Folder == folder || (f.Folder != null && f.Folder.StartsWith(folderPrefix))); | ||
| } |
There was a problem hiding this comment.
Fixed in the restack: folder keys are now stored canonically (backslash-joined) and the list filter matches subfolder prefixes in both separator styles for legacy rows. Regression test added. In #251.
| private static void ReplaceFileContents(string filePath, Func<byte[], byte[]> transform) | ||
| { | ||
| byte[] contents = System.IO.File.ReadAllBytes(filePath); | ||
| string tempPath = filePath + ".tmp"; |
There was a problem hiding this comment.
Declining for now: the 100MB request cap bounds worst-case memory, and this whole-file transform belongs to the CF-compatible AES layer that is already scheduled for an AES-GCM redesign (PLAN-CMS §12.6) — streaming will be designed into that rework rather than bolted onto the layer being replaced.
|
Superseded by the restacked 3-PR stack: #251 (files/photos/import/rate-limit backend) -> #252 (content blocks/left nav/link collections backend) -> #253 (management SPA). These historical cut-point slices predated the branch's later OS-independent path handling and cleanup commits, so they failed CI on Linux runners; the new slices are built from the CI-verified branch tip, carry every review fix (see thread replies), and each passes the full gate set. All feedback on this PR has been answered inline. |
Part 1 of 6 stacked PRs splitting #242 (the full CMS migration, ~24k changed lines) into reviewable, feature-grouped slices that each fit under GitHub Copilot's 20k-line review limit. Merging this PR alone ships the file-management feature; each later PR stacks on the previous one.
Scope
/api/cms/filesbackend:CmsFileService,CmsFileStorageService(folders, save/replace, collision handling, MIME/extension allow-list),CmsFileEncryptionService+CmsFileCrypto(CF-compatible AES so legacy ColdFusion can still read encrypted files),CmsFileAuditService, paged list with filters, upload, edit with permission/person deltas, soft delete/restore./CMS/ManageFiles: server-paged QTable, filters, upload/edit dialog withPermissionSelector/PersonSelector, audit log page, CMS home cards.CMSOptionsControllerfor permission/person typeahead options.The pre-existing
/CMS/Filesdownload handler (Data/CMS.cs) is intentionally kept: it serves all legacy URL shapes (?fn=,?id=,?oldURL=) and was hardened separately under VPR-138/143.Stack: this PR → content blocks/left nav → import/photos/rate limiting → hub/UX → responsive/tests → hardening/final. Each cut point passes the full backend + frontend suites.