fix(medications): keep a custom category when a native client sends OTHER back

An iPhone build that predates custom categories decodes an unknown category
as OTHER and sends it back on every edit. A Bearer update sending OTHER over
a custom category now keeps the custom one; the web can still move it back.
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 22:52:05 +02:00
1 parent 4d7abdd12b
commit 50bc0dab5a
2 files changed
+65 -2

No files matched your search

+22 -2
View File
@@ -285,7 +285,10 @@ export const PUT = apiHandler(
// below excludes every actioned row — and the previous cadence is the one
// genuinely unrecoverable part, which is what C7 puts in the audit row.
// The DELETE beside it stays refused.
const { user } = await requireRecordAuth("manage", "medications");
const { user, authMethod } = await requireRecordAuth(
"manage",
"medications",
);
const { id } = await params;
// v1.5.5 C-E3-3 — route ownership check through the shared helper
@@ -1061,7 +1064,24 @@ export const PUT = apiHandler(
if (projected) medication = { ...medication, ...projected };
}
if (category !== undefined) await setMedicationCategory(id, category);
// v1.40 (#1041) — an iPhone app build that predates custom categories
// decodes an unknown category as OTHER and sends it back on every edit,
// which would overwrite the person's own category with OTHER. A native
// client (Bearer) sending OTHER over a custom category keeps the custom
// one; moving a custom category back to OTHER stays possible on the web.
let categoryToWrite = category;
if (category === "OTHER" && authMethod === "bearer") {
const current = (await resolveMedicationCategories([id]))[id];
if (current?.category.startsWith("custom:")) {
categoryToWrite = undefined;
annotate({
action: { name: "medication.update.custom_category_kept" },
meta: { entity_id: id },
});
}
}
if (categoryToWrite !== undefined)
await setMedicationCategory(id, categoryToWrite);
const resolvedCategory = (await resolveMedicationCategories([id]))[id];
const normalizedCategory = resolvedCategory?.category ?? "OTHER";
@@ -192,6 +192,49 @@ describe("custom medication categories", () => {
expect(body.data).toMatchObject({ category: key, categoryLabel: "Heart" });
});
it("keeps a custom category when an older iPhone build sends OTHER back", async () => {
const key = await createCategory("Heart");
const created = await (await createMedication(key)).json();
const { hashToken } = await import("@/lib/auth/hmac");
const raw = "hlk_custom-category-native-0000";
await getPrismaClient().apiToken.create({
data: {
userId: OWNER,
name: "native",
tokenHash: hashToken(raw),
permissions: ["*"],
},
});
cookieJar.clear();
headerJar.set("authorization", `Bearer ${raw}`);
const { PUT } = await import("@/app/api/medications/[id]/route");
const res = await PUT(
req("PUT", `/api/medications/${created.data.id}`, {
name: "Renamed",
category: "OTHER",
}),
{ params: Promise.resolve({ id: created.data.id }) },
);
const body = await res.json();
expect(res.status, JSON.stringify(body)).toBe(200);
expect(body.data).toMatchObject({ category: key, categoryLabel: "Heart" });
headerJar.clear();
await signIn(OWNER);
});
it("moves a custom category back to OTHER when the web asks", async () => {
const key = await createCategory("Heart");
const created = await (await createMedication(key)).json();
const { PUT } = await import("@/app/api/medications/[id]/route");
const res = await PUT(
req("PUT", `/api/medications/${created.data.id}`, { category: "OTHER" }),
{ params: Promise.resolve({ id: created.data.id }) },
);
const body = await res.json();
expect(res.status, JSON.stringify(body)).toBe(200);
expect(body.data.category).toBe("OTHER");
});
it("renames, hides and caps", async () => {
const key = await createCategory("Heart");
const { PATCH } =