Skip to content

feat(api): add lore entity CRUD routes - #19

Closed
KareemN1936 wants to merge 1 commit into
m2-worlds-seed-bible-hubfrom
feat/lore-crud-api
Closed

KareemN1936 wants to merge 1 commit into
m2-worlds-seed-bible-hubfrom
feat/lore-crud-api

Conversation

@KareemN1936

Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Vercel preview is ready:

https://hivelore-2wkr9j8hr-kareemn1936s-projects.vercel.app

Commit: e8da35b

@mcfarhat

mcfarhat commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

@KareemN1936 this is exactly what I asked for on #18 — you dropped the invented-contract-in-the-frontend approach and landed the backend as its own PR, following the repo's route conventions, with tests that cover the negative cases. That last part matters: anonymous draft read → 404, self-publish → 400, cross-contributor PATCH → 403, delete-canon → 409. Most people write happy-path tests only.

The authorization design is genuinely sound and I tried to break it:

  • No IDOR — every single-entry query is findFirst({where:{id, worldId}}), scoped by both. List scoping ANDs authorId onto {worldId, status}.
  • Self-publish blocked at two layers — z.literal(DRAFT) under .strict() (so PUBLISHED_CANON, hiveReferenceId, publishedAt in the body are all rejected), plus create hardcodes status: DRAFT. PATCH never writes status and 409s on non-draft entries. The schema.prisma:426 invariant holds.
  • Fails closed — query.data.status ?? PUBLISHED_CANON, and non-canon single reads return 404 not 403, so there's no existence oracle. Nice touch.

Blocking:

1. 🔴 Related entries leak drafts to anonymous callers. loreEntryInclude (lore.ts:88-117) pulls incomingRelations/outgoingRelations selecting the related entry's id, loreType, slug, status, title with no status filter, on every route including the anonymous ones (:496 list, :543 read). So fetching a PUBLISHED_CANON entry returns the titles, slugs, IDs and statuses of every DRAFT/ARCHIVED entry linked to it. Latent today (no relationship-creation endpoint yet, though seed.ts:248-266 links two drafts) — but live the moment relationships or publishing ship. Filter relations to PUBLISHED_CANON for unauthorised viewers, or drop relations from the public projection.

2. 🔴 escapeHtml at rest is wrong and diverges from the repo. sanitizeStructuredContent (:174-210) HTML-escapes every string on every write, so a GET → edit → PATCH round-trip turns & into & into & — progressive, irreversible corruption, and it renders as literal entities in the editor. lib/contributions.ts:152 is the pattern to follow: store the JSON verbatim, cap the bytes, escape at render. (This also means #18's dangerouslySetInnerHTML would display raw tags — see my note there.)

3. 🔴 Wrong base — retarget to develop. This currently targets Perla's m2-worlds-seed-bible-hub, which inflates #14's scope and blocks you on her review. I checked the imports: lore.ts needs only config/env, generated/prisma, lib/auth-middleware, lib/prisma, lib/world-authorization, lib/world-permissions — all present on develop. Nothing here needs #14. Retargeting decouples the stacks and unblocks #18 sooner. It also forces the #15 fix now rather than at a messy rebase: on your current base, requireSession/authenticateRequest are still the pre-#15 sync versions without database, i.e. the same bypass you already fixed in #16.

4. Reuse the existing policy helpers. lib/world-resource-policies.ts is on your branch and already exports canEditOwnDraft / canEditAnyDraft typed on AuthoredDraftResource {worldId, authorId, status} — with its own tests. Your hand-rolled canMutateDraft / canReadNonPublicLore / scopeNonPublicLoreList (~100 lines) reimplement it. They're not insecure — they compose the right primitives — but duplicated security logic is how the two copies drift apart later.

5. Add WorldAuditLog writes for create/update/delete, matching createContributionAuditLog (lib/contributions.ts:263-283). Needs LORE_ENTRY_CREATED/UPDATED/DELETED on the WorldAuditAction enum — fold into the same migration.

6. Prisma conflicts become 500s. createUniqueSlug (:264-290) is read-then-write against a unique index → P2002 unhandled. loreEntry.delete (:777) hard-deletes despite LoreRelationship/Proposal/ContributionDraft referencing it with onDelete: Restrict → P2003 unhandled. Map both to 409 via lib/prisma-conflicts.ts; and consider ARCHIVED instead of destroying rows.

7. Enum migration ordering. ADD VALUE without BEFORE/AFTER appends, so the physical order becomes …RULE, OTHER, QUEST, STORY while schema.prisma declares them mid-list. Use AFTER 'FACTION' / AFTER 'EVENT', or move them to the end of the schema block.

8. #18 still won't work after this lands — its pages fetch from Next.js server components with no Authorization header (the token isn't reachable server-side), so a freshly created draft 404s and /contribute?entryId=X dead-ends. Backend right, frontend wrong. Details on #18.

Nits: cap page (:52 is unbounded → huge OFFSET); add a depth cap to the content walker (deep nesting → stack overflow → 500); use a narrow select for the list rather than returning full 100 KB content × 100 entries; replace the bare return; on !request.user (:606,:653,:740) with an explicit 401 — a Fastify handler that returns without replying hangs the request; PATCH regenerates the slug even when the title is unchanged; status in updateLoreBodySchema is accepted then never applied; add the cross-world IDOR test (world-2 is already in your fixture but unused) and a true non-member case; and please write a PR description.

@KareemN1936

Copy link
Copy Markdown
Collaborator Author

Closing this in favor of PR #21.

PR #21 carries the newer integrated lore API implementation, includes the relationship linking work, and has the latest review fixes. Keeping both open would create duplicate/conflicting routes/lore.ts, lore.test.ts, migration, and schema changes.

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