VPR-59 [3/6] CMS migration: import, bulk encrypt, user photos, rate limiting#247
VPR-59 [3/6] CMS migration: import, bulk encrypt, user photos, rate limiting#247rlorenzo wants to merge 3 commits into
Conversation
- POST /api/cms/files/import moves files from the legacy VIPER webroot (CMS:LegacyWebrootPath) into the managed store with per-file results; the original path is stored as oldURL so existing links keep working, matching legacy move-and-track semantics; source paths are contained to the webroot and removed only after a fully successful import - POST /api/cms/files/bulk-encrypt encrypts unencrypted files in place with per-file results; a failed key save rolls the file back to plaintext so disk and DB never disagree, and batch failures clear the change tracker so one bad file cannot poison the rest - UI: Import page (paths textarea keeps failures for retry) and Bulk Encrypt page (multi-select over unencrypted files); list pages now surface fetch errors instead of showing silent empty tables
- /api/cms/photos serves user photos to any authenticated user by
mailId, loginId, or iamId (legacy userPhoto.cfc parity), resolving
ids through AAUD
- altPhoto=true serves the alternate ProfilePhotos/{iamId}.jpg when
present; invalid or traversal-shaped ids fall back to the default
photo without touching disk outside the photo roots
- ID-card photo lookup, caching, and the nopic fallback reuse the
Students area PhotoService rather than duplicating the pipeline
- ZIP requests get a strict per-user token bucket (archives are assembled in memory); single files get a generous sliding window - Buckets key on CAS login, falling back to the proxy-resolved client IP for anonymous requests; rejections return 429 with Retry-After - Limits are configurable under CMS:DownloadRateLimit; invalid values fall back to defaults instead of failing downloads
There was a problem hiding this comment.
Pull request overview
Adds the next slice of the CMS migration by introducing admin tooling for legacy file ingestion/encryption, a CMS-specific user photo API that reuses the Students photo pipeline, and download rate limiting for /CMS/Files to protect memory-heavy ZIP generation.
Changes:
- Add CMS file import + bulk encryption backend (
CmsFileImportService) and expose new endpoints under/api/cms/files. - Add CMS user photo API (
/api/cms/photos/...) with AAUD ID resolution and optional alternate profile-photo lookup. - Add configurable per-user/IP download rate limiting for
/CMS/Files(separate buckets for ZIP vs single-file requests), plus new CMS UI pages/routes for Import + Bulk Encrypt.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web/Program.cs | Registers CMS download rate limiting services and adds UseRateLimiter() after auth. |
| web/Areas/CMS/Services/CmsUserPhotoService.cs | Resolves identifiers via AAUD and serves alternate profile photos with traversal protection, falling back to Students IPhotoService. |
| web/Areas/CMS/Services/CmsFileImportService.cs | Implements legacy-webroot import (move semantics + OldUrl tracking) and bulk encryption with per-file results. |
| web/Areas/CMS/Services/CmsDownloadRateLimiting.cs | Defines the CMS download rate-limit policy and rejection behavior (429 + Retry-After). |
| web/Areas/CMS/Models/DTOs/CmsFileImportDtos.cs | Adds request/response DTOs for import and bulk encryption operations. |
| web/Areas/CMS/Controllers/CMSUserPhotoController.cs | Adds authenticated CMS photo endpoints (by-mail/by-login/by-iam). |
| web/Areas/CMS/Controllers/CMSFilesController.cs | Wires new import and bulk-encrypt endpoints into the CMS file management API. |
| web/Areas/CMS/Controllers/CMSController.cs | Applies rate limiting policy to the /CMS/Files download handler. |
| VueApp/src/CMS/router/routes.ts | Adds CMS routes for the new Import and Bulk Encrypt pages. |
| VueApp/src/CMS/pages/ImportFiles.vue | New UI to submit legacy paths for import and display per-path results. |
| VueApp/src/CMS/pages/Files.vue | Adds a user-visible error notification on failed file list load. |
| VueApp/src/CMS/pages/FileAuditLog.vue | Adds a user-visible error notification on failed audit log load. |
| VueApp/src/CMS/pages/CmsHome.vue | Adds navigation links to Import and Bulk Encrypt admin tools. |
| VueApp/src/CMS/pages/BulkEncrypt.vue | New UI to list unencrypted files, select, confirm, and bulk-encrypt with results display. |
| test/CMS/CmsUserPhotoServiceTests.cs | Unit tests for AAUD resolution + alternate photo lookup + traversal defense. |
| test/CMS/CmsFileImportServiceTests.cs | Unit tests for import containment/move semantics/default permission + bulk encrypt behavior. |
| test/CMS/CmsDownloadRateLimitingTests.cs | Unit tests for ZIP detection and partition-key behavior (login vs IP). |
| $q.dialog({ | ||
| title: "Encrypt Files", | ||
| message: `Encrypt ${selected.value.length} file(s) in place?`, | ||
| cancel: { label: "Cancel", flat: true }, | ||
| persistent: true, | ||
| ok: { label: "Encrypt", color: "primary", unelevated: true }, | ||
| }) |
There was a problem hiding this comment.
Already fixed on the branch: the confirmation uses inflect("file", count) per the project convention. In #253.
| <p | ||
| class="text-body2 text-grey-8" | ||
| style="max-width: 60rem" | ||
| > | ||
| Moves files out of the legacy VIPER webroot into the managed file store. The original path is saved as the |
There was a problem hiding this comment.
Already fixed on the branch: the inline width moved to the shared dialog classes. In #253.
| .LogInformation("Download rate limit hit for {PartitionKey} on {Path}", | ||
| GetPartitionKey(httpContext), | ||
| LogSanitizer.SanitizeString(httpContext.Request.Path)); |
There was a problem hiding this comment.
Fixed in the restack: the partition key is sanitized before logging — for anonymous requests it falls back to a client IP derived from forwardable headers. In #251.
| _context.ChangeTracker.Clear(); | ||
| _storage.DeleteManagedFile(finalPath); | ||
| result.Message = ex.InnerException?.Message ?? ex.Message; | ||
| return result; |
There was a problem hiding this comment.
Already fixed on the branch: source resolution returns fixed messages ('Path is not valid.', 'File not found.', etc.); no exception or inner-exception text reaches the API response. In #251.
|
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 3 of 6 (stacks on #246 — the diff below shows only this slice).
Scope
CmsFileImportService: import files from the legacy webroot (source containment-checked under configurableCMS:LegacyWebrootPath) with move semantics and oldURL tracking, optionalSVMSecure.{folder}default permission; bulk encrypt with per-file results (skips already-encrypted).ImportFiles.vue,BulkEncrypt.vue.CMSUserPhotoController+CmsUserPhotoService(by-mail/by-login/by-iam), serving alternateProfilePhotos\{iamId}.jpgand delegating ID-card photos + nopic fallback to the existing StudentsIPhotoService(no duplicate pipeline)./CMS/Files: strict per-user token bucket for ZIP requests (archives are assembled in memory), generous sliding window for single files; keys on CAS login with proxy-resolved client-IP fallback for anonymous requests; 429 + Retry-After; configurable underCMS:DownloadRateLimitwith safe defaults.