Skip to content

feat(portal): scoped document download (portal-document-download) - #6

Merged
rubenvdlinde merged 1 commit into
developmentfrom
wip/apply-portal-document-download
Jul 23, 2026
Merged

feat(portal): scoped document download (portal-document-download)#6
rubenvdlinde merged 1 commit into
developmentfrom
wip/apply-portal-document-download

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

  • Adds GET /portal/api/collections/{register}/{schema}/{id}/files/{fileId} — streams a file attached to an owned row after the SAME ownership + tenant + trust re-verification as the scoped single-object read (ContributionController::downloadFile()), the read-side counterpart of the existing uploadFile().
  • New opt-in filesDownload: true per collection, normalised fail-closed exactly like filesUpload (PortalManifestNormaliser).
  • Identical-404 discipline: a non-opted-in collection, a foreign-owned/absent row, and a non-existent fileId all return the exact same {"error":"not_found"} body — no existence oracle. PortalFileReader resolves the file strictly within the owned object's own OpenRegister folder and delegates the actual stream + Content-Disposition header to OpenRegister's own FileService::streamFile() (never re-implements that sanitisation).
  • Audit hook (PortalAuditHook) placed on the successful-download path — resolves AuditTrailService by class name via the container (mirrors how PortalFileWriter resolves OpenRegister's FileService), so it is a documented no-op today and starts recording the moment portal-session-hardening-v2 registers the real service.
  • SPA: object() attaches a safe _files listing (id/name/size only) when the collection opts in; the detail view (PageView.jsx) renders a download list, fetched with the bearer auth header (a plain <a href> can't carry it) and saved via a Blob object URL (portalApi.js).
  • Demo exampleCollection (PortalContributionProvider) now also declares filesDownload: true so the feature is exercisable end-to-end alongside the existing filesUpload opt-in.
  • Archived: openspec/specs/supplier-portal/spec.md created (canonical file did not exist yet) with this change's 4 requirements synced in; change dir moved to openspec/changes/archive/2026-07-23-portal-document-download/.

Test plan

  • phpunit -c phpunit-unit.xml — 190 tests (21 new: PortalFileReaderTest, PortalAuditHookTest, PortalManifestNormaliserTest::testFilesDownloadIsCoercedToAStrictBoolean, 8 new ContributionControllerTest download/file-list cases). All 21 new tests pass. The suite's pre-existing 3 errors + 6 failures (PortalSessionServiceTest named-parameter drift, PortalPageControllerTest CSP/template assertions) are verified identical on an origin/development baseline worktree — unrelated to this change, in files not touched here.
  • phpcs — 0 errors/warnings on every touched/new file (fixed two pre-existing-adjacent issues surfaced by the diff: array => re-alignment in PortalContributionProvider.php after adding a longer key, and a missing //end foreach in PortalManifestNormaliser.php). Remaining 11 errors / 27 warnings are all in files this change never touches — verified identical in scope to lib/'s existing debt.
  • phpstan analyse --memory-limit=1G — 0 new errors. The 10 pre-existing errors in PortalSessionService.php (untouched) are verified identical against the origin/development baseline.
  • psalm --no-cache — 0 new errors (12 pre-existing errors in PortalSessionService.php, verified identical against baseline). +2 INFO-level MissingClassConstType notices on the two new services' constants, consistent with the existing untyped-const style already used fleet-wide (e.g. PortalFileWriter::FILE_SERVICE).
  • npm run lint — 0 errors (28 warnings, 4 new — missing JSDoc @param on downloadFile(), matching the pre-existing style gap on sibling methods like uploadFile()).
  • npm run build:portal — compiles cleanly, no warnings.
  • npm run check:specs (json-strict / manifest-v2 / register / registry) — all PASS.
  • openspec validate --all — 28 passed / 3 failed, identical pre-existing failures to baseline (contribution-manifest-v3, portal-contribution-contract, portal-status-transitions — none touched by this change); spec/supplier-portal (this change) is ✓.
  • Playwright e2e (tests/e2e/portal-document-download.spec.ts) — written, not live-run against a running dev instance in this pass (requires debug: true dev-login on a live 8080); see the spec file's header for the manual run command.

🤖 Generated with Claude Code

Closes the portal's upload/download asymmetry: a subject who could already
attach a file to their own row (filesUpload) can now retrieve one back.

- GET /portal/api/collections/{register}/{schema}/{id}/files/{fileId} streams
  a file attached to an owned row after the SAME ownership + tenant + trust
  re-verification as the scoped read (ContributionController::downloadFile()).
- Opt-in `filesDownload: true` per collection, normalised fail-closed exactly
  like the existing `filesUpload` flag.
- Identical-404 discipline: non-opted-in, foreign-owned, and non-existent all
  return the same body — no existence oracle, and the raw stored path is
  never exposed (PortalFileReader delegates streaming to OpenRegister's own
  FileService::streamFile() rather than re-implementing header sanitisation).
- Audit hook placed on the successful-download path (PortalAuditHook),
  resolved by class name so it is a documented no-op until
  portal-session-hardening-v2 registers the real AuditTrailService.
- SPA: object() attaches a safe `_files` listing when a collection opts in;
  the detail view renders a download list (src/portal/components/PageView.jsx,
  portalApi.js) fetched with the bearer auth header and saved via a Blob.
- Unit tests for the normaliser, the file reader, the audit hook, and the
  controller's download path (opt-in gate, ownership-before-stream, identical
  404s, audit invocation); Playwright e2e spec for the live download + 404
  scenarios.
- Archived to openspec/specs/supplier-portal/spec.md (new canonical file).
@rubenvdlinde
rubenvdlinde requested a review from Rem-Dam as a code owner July 23, 2026 18:07
@rubenvdlinde
rubenvdlinde merged commit 3fd2891 into development Jul 23, 2026
18 of 24 checks passed
@rubenvdlinde
rubenvdlinde deleted the wip/apply-portal-document-download branch July 23, 2026 18:08
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/portaliq @ c94517a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 746/746
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-07-23 18:19 UTC

Download the full PDF report from the workflow artifacts.

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.

1 participant