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
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).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
lessonVideoscollection, mirroring theCurriculumTextseparate-collection precedent and the blog moderation lifecycle):LessonVideoMongo doc:lessonNumber, optionalpanelIndex(community videos attach to aTutorialPanelblock; official videos are lesson-level),kind(official|community),submittedBy,submittedAt,status(Pending|Approved|Rejected),title,durationSeconds,storageRef/url,thumbnailUrl(populated later by P5b), audit fieldsreviewedBy/reviewedAt/reviewNotes(mirrorBlogPost)LessonVideoStatusenum inSpikerSoft.Common/EnumsIMongoCollection<LessonVideo>inServiceCollectionExtensions.AddDatabaseConfigurationnearLesson/CurriculumText; lazy indexes (lessonNumber, status) in an owning serviceEndpoints 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 startsPendingGET /api/Lessons/videos/pending—[Authorize(Roles = "staff,admin")], moderation queuePOST /api/Lessons/videos/{id}/approve—[Authorize(Roles = "staff,admin")], requiresPending, setsApproved+ reviewer auditPOST /api/Lessons/videos/{id}/reject—[Authorize(Roles = "staff,admin")], requiresPending, setsRejected+ reasonGET /api/Lessons/{lessonNumber:int}/videos— anonymous-readable, returns onlyApprovedvideos (official for the lesson + approved community grouped by panel). Never leaks pending/rejectedMediatR: 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).
Done — merged via spikersoft-backend PR #57.
LessonVideomodel +LessonVideoKind/LessonVideoStatusenums, submit + moderation (approve/reject) MediatR handlers, approved-only anonymous read path,IMongoCollection<LessonVideo>DI, and 16 unit tests. Closing.