[Playgrounds P5a] Videos backend foundation — LessonVideo model, submission + moderation endpoints, approved-read path #336

Closed
opened 2026-07-02 15:14:47 +00:00 by spikerj · 1 comment
Owner

First sub-ticket of P5 (#328), epic #323. Backend data + API foundation for the videos layer. No media transcode/upload pipeline here — that is P5b; this slice accepts a storage ref / URL and models the moderation lifecycle so the frontend and pipeline can build against a stable contract.

Model (separate lessonVideos collection, mirroring the CurriculumText separate-collection precedent and the blog moderation lifecycle):

  • LessonVideo Mongo doc: lessonNumber, optional panelIndex (community videos attach to a TutorialPanel block; official videos are lesson-level), kind (official | community), submittedBy, submittedAt, status (Pending | Approved | Rejected), title, durationSeconds, storageRef/url, thumbnailUrl (populated later by P5b), audit fields reviewedBy/reviewedAt/reviewNotes (mirror BlogPost)
  • LessonVideoStatus enum in SpikerSoft.Common/Enums
  • Register IMongoCollection<LessonVideo> in ServiceCollectionExtensions.AddDatabaseConfiguration near Lesson/CurriculumText; lazy indexes (lessonNumber, status) in an owning service

Endpoints on LessonsController (Lessons domain):

  • POST /api/Lessons/{lessonNumber:int}/videos[Authorize], submit a community video (metadata + storage ref); official videos seeded/created by staff. Captures submitter id; status starts Pending
  • GET /api/Lessons/videos/pending[Authorize(Roles = "staff,admin")], moderation queue
  • POST /api/Lessons/videos/{id}/approve[Authorize(Roles = "staff,admin")], requires Pending, sets Approved + reviewer audit
  • POST /api/Lessons/videos/{id}/reject[Authorize(Roles = "staff,admin")], requires Pending, sets Rejected + reason
  • GET /api/Lessons/{lessonNumber:int}/videos — anonymous-readable, returns only Approved videos (official for the lesson + approved community grouped by panel). Never leaks pending/rejected

MediatR: queries/commands under Domain/Lessons/Queries/GetLessonVideos, Queries/GetPendingLessonVideos, Commands/SubmitLessonVideo, Commands/ApproveLessonVideo, Commands/RejectLessonVideo, mirroring the blog moderation handlers.

Tests: submit sets Pending + submitter; approve/reject transition guards (only from Pending) + audit stamping; read path returns only Approved and never pending/rejected; pending queue is staff-gated.

Acceptance: staff can approve/reject with an audit trail; the anonymous read path exposes only approved videos; contract is stable for P5b (pipeline) and P5d (playground UI).

First sub-ticket of P5 (#328), epic #323. Backend data + API foundation for the videos layer. **No media transcode/upload pipeline here** — that is P5b; this slice accepts a storage ref / URL and models the moderation lifecycle so the frontend and pipeline can build against a stable contract. **Model** (separate `lessonVideos` collection, mirroring the `CurriculumText` separate-collection precedent and the blog moderation lifecycle): - [ ] `LessonVideo` Mongo doc: `lessonNumber`, optional `panelIndex` (community videos attach to a `TutorialPanel` block; official videos are lesson-level), `kind` (`official` | `community`), `submittedBy`, `submittedAt`, `status` (`Pending` | `Approved` | `Rejected`), `title`, `durationSeconds`, `storageRef`/`url`, `thumbnailUrl` (populated later by P5b), audit fields `reviewedBy`/`reviewedAt`/`reviewNotes` (mirror `BlogPost`) - [ ] `LessonVideoStatus` enum in `SpikerSoft.Common/Enums` - [ ] Register `IMongoCollection<LessonVideo>` in `ServiceCollectionExtensions.AddDatabaseConfiguration` near `Lesson`/`CurriculumText`; lazy indexes (lessonNumber, status) in an owning service **Endpoints** on `LessonsController` (Lessons domain): - [ ] `POST /api/Lessons/{lessonNumber:int}/videos` — `[Authorize]`, submit a community video (metadata + storage ref); official videos seeded/created by staff. Captures submitter id; status starts `Pending` - [ ] `GET /api/Lessons/videos/pending` — `[Authorize(Roles = "staff,admin")]`, moderation queue - [ ] `POST /api/Lessons/videos/{id}/approve` — `[Authorize(Roles = "staff,admin")]`, requires `Pending`, sets `Approved` + reviewer audit - [ ] `POST /api/Lessons/videos/{id}/reject` — `[Authorize(Roles = "staff,admin")]`, requires `Pending`, sets `Rejected` + reason - [ ] `GET /api/Lessons/{lessonNumber:int}/videos` — anonymous-readable, returns only `Approved` videos (official for the lesson + approved community grouped by panel). Never leaks pending/rejected **MediatR**: queries/commands under `Domain/Lessons/Queries/GetLessonVideos`, `Queries/GetPendingLessonVideos`, `Commands/SubmitLessonVideo`, `Commands/ApproveLessonVideo`, `Commands/RejectLessonVideo`, mirroring the blog moderation handlers. **Tests**: submit sets Pending + submitter; approve/reject transition guards (only from Pending) + audit stamping; read path returns only Approved and never pending/rejected; pending queue is staff-gated. **Acceptance:** staff can approve/reject with an audit trail; the anonymous read path exposes only approved videos; contract is stable for P5b (pipeline) and P5d (playground UI).
Author
Owner

Done — merged via spikersoft-backend PR #57. LessonVideo model + LessonVideoKind/LessonVideoStatus enums, submit + moderation (approve/reject) MediatR handlers, approved-only anonymous read path, IMongoCollection<LessonVideo> DI, and 16 unit tests. Closing.

Done — merged via spikersoft-backend PR #57. `LessonVideo` model + `LessonVideoKind`/`LessonVideoStatus` enums, submit + moderation (approve/reject) MediatR handlers, approved-only anonymous read path, `IMongoCollection<LessonVideo>` DI, and 16 unit tests. Closing.
Sign in to join this conversation.