Skip to content

BE_14 - PDF Retrieval and Real-time Display #34

@notjackl3

Description

@notjackl3

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]/slides endpoint (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]/navigate endpoint
  • 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 expiresAt ISO 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_state event
  • 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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions