-
Notifications
You must be signed in to change notification settings - Fork 1
Open
Labels
Description
Summary
Implement secure PDF retrieval with signed URLs and real-time slide synchronization so all session participants view the same slides together.
[Estimated hours: 6-8]
Objectives
- Create GET
/api/sessions/[sessionId]/slidesendpoint (list all slide sets) - Create GET
/api/sessions/[sessionId]/slides/[slideSetId]endpoint (retrieve PDF with signed URL) - Generate time-limited signed URLs for secure PDF access
- Create POST
/api/sessions/[sessionId]/slides/[slideSetId]/navigateendpoint - Implement Socket.IO handlers for real-time slide synchronization
- Track and broadcast current slide state to all participants
- Validate user membership before providing access
Description
After a professor uploads slides, all session participants need to retrieve and view the PDF in sync. This ticket implements secure PDF retrieval with time-limited access URLs and real-time slide synchronization where all participants see the same slide when the professor navigates.
Complete Flow
Step 1: Initial PDF Load
Student joins session
→ GET /api/sessions/:sessionId/slides
→ Backend validates session membership
→ Returns list of available slide sets
→ Frontend displays slide set selector
Student/Professor selects slide set
→ GET /api/sessions/:sessionId/slides/:slideSetId
→ Backend validates session membership
→ Generate signed URL (expires in 1 hour)
→ Return PDF URL and slide metadata
→ Frontend loads PDF in @embedpdf viewer
→ Connect to Socket.IO for real-time updates
→ Join room session:{sessionId}
→ Receive current slide state (if professor already navigated)
Step 2: Real-time Synchronization
Professor navigates to slide 5
→ POST /api/sessions/:sessionId/slides/:slideSetId/navigate
→ Backend validates professor role
→ Update session's current slide state in database
→ Broadcast to session:{sessionId} room via Socket.IO
→ All participants' PDF viewers jump to slide 5
Technical Details
File Structure
src/app/api/sessions/[sessionId]/
├── slides/
│ └── route.ts # GET list handler
└── slides/[slideSetId]/
├── route.ts # GET single + signed URL
└── navigate/
└── route.ts # POST navigation handler
src/lib/
├── slideRetrieval.ts # Slide fetching and signed URL logic
├── slideNavigation.ts # Navigation state management
└── signedUrl.ts # Signed URL generation utilities
src/socket/handlers/
└── slideHandlers.ts # Socket.IO broadcast functions
Functions:
- broadcastSlideChange(io, sessionId, slideData)
- sendCurrentStateToUser(socket, sessionId)
Database Schema Updates
Add SlideSet model (revises previous ticket):
model SlideSet {
id String @id @default(cuid())
sessionId String
session Session @relation(fields: [sessionId], references: [id])
filename String
storageKey String // Identifier in storage system
storageUrl String // Access URL
pageCount Int
fileSize Int // Bytes
uploadedBy String
uploader User @relation(fields: [uploadedBy], references: [id])
slides Slide[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sessionId])
}Update Slide model:
model Slide {
id String @id @default(cuid())
slideSetId String
slideSet SlideSet @relation(fields: [slideSetId], references: [id])
pageNumber Int
session Session @relation(fields: [sessionId], references: [id])
questions Question[]
@@unique([slideSetId, pageNumber])
@@index([slideSetId])
}Update Session model for current slide tracking:
model Session {
id String @id @default(cuid())
// ... existing fields ...
currentSlideSetId String?
currentSlideSet SlideSet? @relation(fields: [currentSlideSetId], references: [id])
currentSlideId String?
currentSlide Slide? @relation(fields: [currentSlideId], references: [id])
slideSets SlideSet[]
slides Slide[]
questions Question[]
@@index([currentSlideSetId])
@@index([currentSlideId])
}Update User model:
model User {
// ... existing fields ...
uploadedSlideSets SlideSet[]
}Acceptance Criteria
- Only session members can access slides (403 for non-members)
- Signed URL expires after 1 hour with cryptographic security (use crypto.createHmac)
- Response includes
expiresAtISO timestamp for frontend tracking - Non-existent slideSetId returns 404
- List endpoint returns all slide sets for session ordered by createdAt desc
- Only professors can trigger slide navigation (403 for TAs and students)
- Slide change broadcasts to all connected participants in room within 500ms
- Participants joining mid-session receive current slide state immediately via
current_stateevent - slideId must belong to specified slideSetId (400 if mismatch)
- Socket.IO connections authenticated and tied to session membership
- Signed URL generation uses environment secret (e.g., SIGNED_URL_SECRET)
- Follow existing validation pattern (ValidationResult interface)
- Error messages follow existing API error format
- Frontend receives warning before signed URL expires (client-side implementation)
Notes
- This ticket requires the SlideSet model from the previous upload ticket
- Uses existing Socket.IO infrastructure in
/src/socket/handlers/ - Follows existing room naming convention (
session:{sessionId}) - Integrates with existing @embedpdf viewer packages
Reactions are currently unavailable