Skip to content

VPR-59 [1/6] CMS migration: file management (API + UI)#245

Closed
rlorenzo wants to merge 2 commits into
mainfrom
VPR-59-cms-1-files
Closed

VPR-59 [1/6] CMS migration: file management (API + UI)#245
rlorenzo wants to merge 2 commits into
mainfrom
VPR-59-cms-1-files

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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/files backend: 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.
  • Files UI at /CMS/ManageFiles: server-paged QTable, filters, upload/edit dialog with PermissionSelector/PersonSelector, audit log page, CMS home cards.
  • CMSOptionsController for permission/person typeahead options.
  • Unit tests for crypto, storage, service, and controller.

The pre-existing /CMS/Files download 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.

rlorenzo added 2 commits July 2, 2026 14:38
- 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
@codecov-commenter

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 21.74kB (1.02%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.15MB 21.74kB (1.02%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/GenericError-*.js -6.64kB 531.89kB -1.23%
assets/TermManagement-*.js -6 bytes 52.38kB -0.01%
assets/schedule-*.js 5 bytes 49.73kB 0.01%
assets/CourseDetail-*.js 4 bytes 39.33kB 0.01%
assets/PhotoGallery-*.js 34 bytes 36.12kB 0.09%
assets/InstructorEdit-*.js -3 bytes 28.55kB -0.01%
assets/InstructorList-*.js 35 bytes 25.95kB 0.14%
assets/effort-*.js 37 bytes 23.21kB 0.16%
assets/CrossListedCoursesSection-*.js -5 bytes 22.29kB -0.02%
assets/MultiYearReport-*.js 1 bytes 18.75kB 0.01%
assets/EmergencyContactForm-*.js 2 bytes 17.16kB 0.01%
assets/MyAssessments-*.js 32 bytes 16.51kB 0.19%
assets/Files-*.js (New) 15.32kB 15.32kB 100.0% 🚀
assets/permission-*.js 36 bytes 9.59kB 0.38%
assets/CourseLinkDialog-*.js -5 bytes 9.37kB -0.05%
assets/cts-*.js 37 bytes 8.37kB 0.44%
assets/AssessmentList-*.js 37 bytes 6.83kB 0.54%
assets/UserStore-*.js (New) 6.79kB 6.79kB 100.0% 🚀
assets/WebReports-*.js 5 bytes 6.37kB 0.08%
assets/UnitList-*.js 1 bytes 6.35kB 0.02%
assets/AssessmentEpa-*.js 37 bytes 5.67kB 0.66%
assets/FileAuditLog-*.js (New) 4.03kB 4.03kB 100.0% 🚀
assets/students-*.js 32 bytes 3.23kB 1.0%
assets/PercentAssignTypeList-*.js -5 bytes 2.67kB -0.19%
assets/PercentAssignTypeInstructors-*.js -5 bytes 2.4kB -0.21%
assets/CtsHome-*.js 37 bytes 2.25kB 1.67%
assets/cms-*.js 352 bytes 1.72kB 25.67% ⚠️
assets/cahfs-*.js 5 bytes 1.65kB 0.3%
assets/CmsHome-*.js 1.3kB 1.62kB 416.61% ⚠️
assets/clinicalscheduler-*.js 5 bytes 1.57kB 0.32%
assets/computing-*.js 5 bytes 979 bytes 0.51%
assets/CAHFSAuth-*.js 37 bytes 626 bytes 6.28% ⚠️
assets/CheckPagePermission-*.js -3 bytes 149 bytes -1.97%
assets/FileAuditLog-*.css (New) 100 bytes 100 bytes 100.0% 🚀
assets/Files-*.css (New) 98 bytes 98 bytes 100.0% 🚀

Files in assets/GenericError-*.js:

  • ./src/composables/ViperFetch.ts → Total Size: 6.9kB

Files in assets/Files-*.js:

  • ./src/CMS/components/FileFormDialog.vue → Total Size: 150 bytes

  • ./src/CMS/components/PermissionSelector.vue → Total Size: 162 bytes

  • ./src/CMS/pages/Files.vue → Total Size: 203 bytes

  • ./src/CMS/components/PersonSelector.vue → Total Size: 150 bytes

Files in assets/FileAuditLog-*.js:

  • ./src/CMS/pages/FileAuditLog.vue → Total Size: 224 bytes

Files in assets/cms-*.js:

  • ./src/CMS/router/routes.ts → Total Size: 1.42kB

Files in assets/CmsHome-*.js:

  • ./src/CMS/pages/CmsHome.vue → Total Size: 124 bytes

@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.63218% with 386 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.94%. Comparing base (713e472) to head (75fd058).

Files with missing lines Patch % Lines
web/Areas/CMS/Controllers/CMSFilesController.cs 0.00% 98 Missing ⚠️
web/Areas/CMS/Services/CmsFileService.cs 75.07% 62 Missing and 25 partials ⚠️
web/Areas/CMS/Services/CmsFileAuditService.cs 0.00% 75 Missing ⚠️
web/Areas/CMS/Services/CmsFileEncryptionService.cs 0.00% 47 Missing ⚠️
web/Areas/CMS/Controllers/CMSOptionsController.cs 0.00% 41 Missing ⚠️
web/Areas/CMS/Services/CmsFileStorageService.cs 88.67% 6 Missing and 6 partials ⚠️
VueApp/src/composables/ViperFetch.ts 11.11% 8 Missing ⚠️
web/Areas/CMS/Services/CmsFileCrypto.cs 88.67% 6 Missing ⚠️
web/Areas/CMS/Constants/CmsFileTypes.cs 87.50% 5 Missing ⚠️
web/Areas/CMS/Data/CMS.cs 0.00% 5 Missing ⚠️
... and 1 more
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     
Flag Coverage Δ
backend 45.03% <56.09%> (+0.31%) ⬆️
frontend 43.27% <11.11%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.


private static IFormFile MakeFormFile(string fileName = "report.pdf")
{
var stream = new MemoryStream(new byte[] { 1, 2, 3 });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed together with the code-quality finding above — streams are tracked and disposed by the fixture. Carried into #251.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 useFetch helpers.

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.

Comment on lines +65 to +69
if (val.trim().length < 2) {
update(() => {
options.value = []
})
return

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +103 to +105
// 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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +67 to +70
// 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)));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +62 to +65
private static void ReplaceFileContents(string filePath, Func<byte[], byte[]> transform)
{
byte[] contents = System.IO.File.ReadAllBytes(filePath);
string tempPath = filePath + ".tmp";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rlorenzo

rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

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.

@rlorenzo rlorenzo closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants