fix(mood): assert tag-link entry ownership and guard mood migrations with IF NOT EXISTS

Thread the acting user id through createTagLinks, replaceTagLinks, and
replaceRatedFactorLinks, and assert the target MoodEntry belongs to that
user before writing any link. The check runs against the same client (tx
or singleton) as the write, so it shares one snapshot. Behaviour is
unchanged for the existing POST and bulk paths, where the entry is freshly
created and owned; the guard closes a latent cross-user link hazard for
the planned PATCH mood-edit route. A mismatched or missing entry throws
MoodEntryOwnershipError before any catalog or join touch.

Add IF NOT EXISTS to every ADD COLUMN in migration 0119 so a clean replay
is idempotent, matching the 0117 convention.

(cherry picked from commit 1bf983f6c2c5e44796c99983d57c1b1d90cd7099)
This commit is contained in:
Marc-André Bombeck committed 2026-06-05 01:05:47 +02:00
1 parent cfb97593a3
commit e77e8ae705
8 files changed
+110 -11

No files matched your search

+1 -1
View File
@@ -150,7 +150,7 @@ export const PUT = apiHandler(
// is present in the body. `null` clears every link; an omitted
// field leaves the links untouched.
if (data.tagKeys !== undefined) {
await replaceTagLinks(id, data.tagKeys ?? [], tx);
await replaceTagLinks(id, user.id, data.tagKeys ?? [], tx);
}
// v1.8.5 — read the persisted link keys back so the update
@@ -251,6 +251,7 @@ describe("POST /api/mood-entries — entry + tag-links transaction (v1.8.5)", ()
// arg is the rated-factor set (empty here — binary tag only).
expect(createTagLinks).toHaveBeenCalledWith(
"mood-1",
"user-1",
["happy"],
txClient,
[],
@@ -331,6 +332,7 @@ describe("POST /api/mood-entries — rated factors (v1.12.0)", () => {
expect(res.status).toBe(201);
expect(createTagLinks).toHaveBeenCalledWith(
"mood-rf",
"user-1",
["happy"],
txClient,
[{ key: "factor_work", rating: 4 }],
@@ -207,6 +207,7 @@ describe("POST /api/mood-entries/bulk — structured tagKeys (v1.12.0)", () => {
// outside the upsert tx) and the rated-factor set (4th, empty here).
expect(createTagLinks).toHaveBeenCalledWith(
"entry-1",
"user-1",
["movies", "gaming"],
prisma,
[],
@@ -257,6 +258,7 @@ describe("POST /api/mood-entries/bulk — structured tagKeys (v1.12.0)", () => {
expect(json.data.inserted).toBe(1);
expect(createTagLinks).toHaveBeenCalledWith(
"entry-1",
"user-1",
["movies"],
prisma,
[{ key: "factor_work", rating: 4 }],
+1
View File
@@ -249,6 +249,7 @@ async function postBulk(request: NextRequest): Promise<Response> {
) {
await createTagLinks(
result.id,
user.id,
entry.tagKeys ?? [],
prisma,
entry.ratedFactors ?? [],
+1
View File
@@ -214,6 +214,7 @@ async function postMoodEntry(request: NextRequest) {
// throws `RatedFactorOutOfRangeError`, rolling the tx back.
await createTagLinks(
created.id,
user.id,
tagKeys ?? [],
tx,
ratedFactors ?? [],