diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index b9ca05fd..6295b136 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -2908,9 +2908,17 @@ const upsertFontSettingsHandler = async ( } } const targetUserId = data.scope === "global" ? null : userId; - // Logo is a brand asset and only lives on the global row. Per-user - // PATCHes silently drop the field rather than 400-ing so a generic - // "save my prefs" UI doesn't have to special-case the toggle. + // Logo is a brand asset and only lives on the global row. Reject + // user-scope writes that try to set a non-null logo so callers get + // a clear error instead of a silent no-op. `null` (explicit clear) + // and `undefined` (field not provided) are both fine on user rows. + if (data.scope === "user" && data.logoObjectPath != null) { + res.status(400).json({ + error: "logoObjectPath is only allowed on the global font-settings row", + code: "logo_user_scope_forbidden", + }); + return; + } const logoForWrite = data.scope === "global" ? data.logoObjectPath : undefined; const result = await db.transaction(async (tx) => { const [existing] = await tx diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index f577a010..7abca605 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -696,6 +696,49 @@ test("Font settings: valid combo 200; invalid weight/size/family 400", async () assert.equal(badFamily.status, 400); }); +test("Font settings: user-scope rejects logoObjectPath with 400; null is allowed", async () => { + // User-scope writes that try to set a non-null logo path must be + // rejected loudly. The brand logo is a global asset; silently + // dropping the field would mislead callers into thinking their + // upload was saved. + const rejected = await api( + adminCookie, + "PUT", + "/api/executive-meetings/font-settings", + { + scope: "user", + fontFamily: "system", + fontSize: 14, + fontWeight: "regular", + alignment: "start", + fontColor: "#000000", + logoObjectPath: "/objects/some-fake-id", + }, + ); + assert.equal(rejected.status, 400); + const body = await rejected.json(); + assert.equal(body.code, "logo_user_scope_forbidden"); + + // Explicit null (i.e. "I'm not setting a logo") must still work + // on the user row so a generic prefs UI doesn't have to omit the + // field. + const okNull = await api( + adminCookie, + "PUT", + "/api/executive-meetings/font-settings", + { + scope: "user", + fontFamily: "system", + fontSize: 14, + fontWeight: "regular", + alignment: "start", + fontColor: "#000000", + logoObjectPath: null, + }, + ); + assert.equal(okNull.status, 200); +}); + test("PDF archives: POST creates a snapshot and GET returns it", async () => { const post = await api( adminCookie,