Skip to content

Add presentation versioning with per-version reviews#654

Open
vanbroup wants to merge 7 commits into
mainfrom
worktree-presentation-versions
Open

Add presentation versioning with per-version reviews#654
vanbroup wants to merge 7 commits into
mainfrom
worktree-presentation-versions

Conversation

@vanbroup

Copy link
Copy Markdown
Contributor

Summary

  • Each presentation upload now creates an immutable row in presentation_versions rather than overwriting presentation_r2_key on session_proposals
  • The three old upload-tracking columns (presentation_r2_key, presentation_uploaded_at, presentation_uploaded_by_user_id) are backfilled and dropped in migration 0033
  • Per-version reviews allow the programme committee to approve, reject, or request revisions on each upload independently

New endpoints (OpenAPI-registered, bearer-token capable for MCP)

Method Path Notes
GET /admin/proposals/:id/presentation/versions List all versions with latest review
GET /admin/proposals/:id/presentation/versions/:id/download Stream file from R2
POST /admin/proposals/:id/presentation/versions/:id/review Requires presentations:review scope
DELETE /admin/proposals/:id/presentation/versions/:id Requires presentations:delete scope; blocks deleting the only approved version
GET /proposals/speaker/:token/presentation/download Speaker self-download of current version

New scopes

presentations:review, presentations:delete

Admin UI

The proposal detail page gains a Presentation tab (shown only when the session type has requiresPresentation: true) with version cards showing upload time, file size, latest review status, download link, inline review form, and soft-delete.

Test plan

  • Upload a presentation as a speaker — verify a presentation_versions row is created with is_current = 1
  • Upload a second presentation — verify previous version gets is_current = 0, new one gets is_current = 1
  • Admin: open Presentation tab, download a version, submit a review
  • Admin: attempt to delete the only approved version — verify 409 error
  • Speaker: click "Go to presentation upload" link — verify correct URL with token
  • Speaker: use download endpoint to retrieve their current upload
  • Run migration against a DB with existing presentation data — verify version 1 rows are backfilled

🤖 Generated with Claude Code

vanbroup and others added 2 commits June 25, 2026 13:28
Each presentation upload now creates an immutable version row in
presentation_versions rather than overwriting the previous file reference.
The current version is tracked via is_current; old versions are retained
for audit and review purposes.

New endpoints (all OpenAPI-registered, bearer-token capable for MCP):
  GET    /admin/proposals/:id/presentation/versions          — list versions
  GET    /admin/proposals/:id/presentation/versions/:id/download
  POST   /admin/proposals/:id/presentation/versions/:id/review
  DELETE /admin/proposals/:id/presentation/versions/:id      — requires presentations:delete scope
  GET    /proposals/speaker/:token/presentation/download     — speaker self-download

New scopes: presentations:review, presentations:delete

Admin proposal detail gains a "Presentation" tab (shown only when the
session type requires a presentation) with version cards, download links,
inline review form, and soft-delete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The three columns added for tracking the current presentation
(presentation_r2_key, presentation_uploaded_at, presentation_uploaded_by_user_id)
are superseded by presentation_versions. Migration 0033 now backfills
existing uploads as version 1 rows and drops the old columns.

All code that read from those columns is updated to query
presentation_versions instead:
- getSpeakerByManageToken: removes dropped columns from SQL + type
- getPresentationUploader: joins presentation_versions instead of session_proposals
- reminders: NOT EXISTS subquery replaces presentation_uploaded_at IS NULL
- admin speakers endpoint: LEFT JOINs presentation_versions for upload status
- remind-presentation endpoints: LEFT JOINs pv.id for hasPresentation flag
- speaker token endpoint: presentationUploaded/At derived from uploader result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces versioned presentation uploads and reviews for session proposals, replacing the previous single-upload fields with dedicated presentation_versions and presentation_version_reviews tables. It adds a new "Presentation" tab to the admin interface and updates the speaker-facing APIs to support multi-version history. The review feedback highlights several critical security and reliability issues, including Insecure Direct Object Reference (IDOR) vulnerabilities across multiple admin endpoints, potential HTTP Header Injection in file downloads due to unescaped filenames, a race condition in version number generation, a logical inconsistency when soft-deleting current versions, and a potential migration failure due to NOT NULL constraints on migrated timestamps.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread functions/api/v1/proposals/speaker/[token]/presentation/download.ts
Comment thread functions/_lib/services/presentation-versions.ts Outdated
Comment thread functions/_lib/services/presentation-versions.ts Outdated
Comment thread migrations/0033_presentation_versions.sql
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 25, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
pkic-org 9ba7a3f Commit Preview URL

Branch Preview URL
Jun 25 2026, 04:08 PM

vanbroup and others added 5 commits June 25, 2026 13:35
Security (IDOR): all three version-scoped endpoints now verify that the
requested version belongs to the proposalId in the URL path, returning 404
if not — prevents cross-proposal ID harvesting.

Security (header injection): both download endpoints now sanitise the
filename before embedding it in Content-Disposition and add the RFC 5987
filename* parameter for safe UTF-8 names.

Race condition: version number generation is now an atomic subquery inside
the INSERT rather than a separate SELECT + application-level increment,
eliminating the UNIQUE constraint race on concurrent uploads.

Soft-delete consistency: deletePresentationVersion now sets is_current = 0
on the deleted row in the same UPDATE, keeping is_current logically
consistent even without the deleted_at filter.

Migration safety: uploaded_at in the backfill INSERT now uses
COALESCE(presentation_uploaded_at, updated_at, submitted_at) to handle
any rows where the upload timestamp was never recorded.

Data cleanliness: review note is trimmed and coerced to null when blank.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests cover all seven scenarios from the test plan:
1. First upload creates a version row with is_current = 1
2. Second upload promotes the new version and demotes the previous one
3. Admin can list versions, download a file, and submit a review
4. Admin is blocked from deleting an approved version (409 CANNOT_DELETE_APPROVED)
5. Speaker GET response includes a presentationUrl field (with token) inside proposal
6. Speaker can download their current presentation via the download endpoint
7. Migration columns (presentation_r2_key etc.) are absent post-migration, confirming
   the backfill ran and data is accessible through presentation_versions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix finalize.ts to call processOutboxByIdBackground after each
  queueEmail so decision emails are actually delivered in wrangler dev
- Add DEFAULT_MIN_PROPOSAL_REVIEWS=0 to e2e-start.sh env-file so the
  finalize step succeeds without requiring 2 reviews in the E2E env
- Add missing presentation/versions mock route to admin-proposal-detail
  spec so the auth token is not cleared on the unmocked 401 response
- Add full real-server E2E test: speaker uploads PDF, admin views
  Presentation tab and submits review, speaker downloads file
- Fix reminder-cycle test: replace dropped presentation_uploaded_at
  column reference with INSERT into presentation_versions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…behalf

Adds POST /api/v1/admin/proposals/:proposalId/presentation/versions so
admins can upload a file directly without needing the speaker token.
The Presentation tab now shows an "Upload on behalf of speaker" button
in both the empty state and above the versions list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ication

Move file validation (MIME type, size) and R2 write into
parsePresentationUpload() + storePresentationFile() in the
presentation-versions service. Both the speaker and admin upload
endpoints now call these shared helpers instead of duplicating the logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@svenska-primekey

Copy link
Copy Markdown
Collaborator

Is this ready to merge @vanbroup?

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.

2 participants