Merge remote-tracking branch 'origin/fix/delegated-fix2' into work/sharing-write-integration

This commit is contained in:
Marc-André Bombeck committed 2026-08-03 21:15:01 +02:00
commit b817a2d47e
25 files changed
+956 -204

No files matched your search

+32 -5
View File
@@ -11,7 +11,7 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
@@ -67,10 +67,37 @@ export const GET = apiHandler(async (request: NextRequest) => {
export const POST = apiHandler(withIdempotency<[NextRequest]>(postAllergy));
async function postAllergy(request: NextRequest): Promise<Response> {
// v1.36.x — a delegated write, and the cleanest of them: the row is a plain
// statement about the record's own body, and the caller appears in the audit
// trail rather than in the row.
const { user } = await requireRecordAuth("write");
// v1.36.x — the GET above is delegable and this is not, which is the
// opposite of where the classification landed and worth the paragraph.
//
// Nothing about the row changed: it is still a plain statement about the
// record's own body, still the single most useful thing a caregiver could
// contribute, and the argument for admitting it still holds. What it does
// not have is a caller. The only place in the product that posts here is the
// allergy manager in Settings → Anamnese, and `/settings/*` is not a
// shared-record destination — the shell shows the "not part of what was
// shared" panel there, so no delegate can reach the form at any level.
//
// An admitted write with no reachable surface is the one-ended change this
// repository keeps rediscovering (CLAUDE.md, "A two-ended change carries
// both ends"): the permission ships, the consumer is the follow-up, and
// nothing in the gate notices because every other check proves the other
// end. The frozen list is built the other way round on purpose — its own
// actor-surface note says the rest "arrive as their own diffs; naming them
// before they exist would freeze a guess."
//
// So this arm waits for the surface that would exercise it, and the two
// land together. Choosing that surface is design work, not a fix: allergies
// and family history have exactly one home today, that home is a personal
// account surface a switch rightly closes, and bolting a second copy onto a
// shared page would split one concept across two places. Re-admitting is
// one line here plus one entry in `delegable-surface-guard.test.ts` plus the
// paragraph that argues it — which is exactly the reviewed diff that guard
// exists to force.
//
// The half that was always the point is untouched: a caregiver can still
// READ the allergy list inside the record.
const { user } = await requireAuth();
const { data: rawBody, error: jsonError } = await safeJson(request, {
maxBytes: 16 * 1024,
+17 -6
View File
@@ -12,7 +12,7 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
@@ -62,11 +62,22 @@ export const POST = apiHandler(
);
async function postFamilyHistory(request: NextRequest): Promise<Response> {
// v1.36.x — a delegated write, and the one where third-party health data is
// present by design: the row describes the record's relatives. It is stored
// as the record's own, exactly as the delegable READ arm above already
// serves it, and the audit trail names who entered it.
const { user } = await requireRecordAuth("write");
// v1.36.x — not a delegated write, though the GET above is delegable. The
// reasoning is written out at the sibling arm in `api/allergies/route.ts`
// and is the same here: the only surface that posts to either route is the
// manager in Settings → Anamnese, and `/settings/*` is closed inside a
// shared record, so no delegate can reach the form at any level. An
// admitted write with no reachable caller is a permission frozen ahead of
// the surface that would exercise it.
//
// This arm carried an extra reason to wait. The row describes the record's
// RELATIVES, so it is the one admitted write where third-party health
// information is present by design — and frequently the delegate is that
// relative, asserting a condition about themselves into somebody else's
// record. The classification admitted it and named the discomfort. Landing
// it together with the surface that offers it means the copy on that surface
// can say whose statement the row is, which no route comment can.
const { user } = await requireAuth();
const { data: rawBody, error: jsonError } = await safeJson(request, {
maxBytes: 16 * 1024,
+10 -1
View File
@@ -157,6 +157,7 @@ export default function MeasurementsPage() {
actions={
canAdd ? (
<Button
data-slot="measurement-add"
className="min-h-11 sm:min-h-9"
onClick={() => {
setReturnTo(null);
@@ -170,8 +171,16 @@ export default function MeasurementsPage() {
}
/>
{/* v1.36.x — the sheet answers the same question the header button
answers. `?add=<TYPE>` opens it without passing the button, and a
deep link is the same affordance as the control that produces it, so
it gets the same gate. Gating the open rather than only the param
also covers the first-paint window: `canAdd` reads true until
`/api/auth/me` settles, and a sheet opened in that frame withdraws
when the answer lands instead of standing on a form the server
refuses. */}
<ResponsiveSheet
open={dialogOpen}
open={dialogOpen && canAdd}
onOpenChange={(open) => {
setDialogOpen(open);
if (!open) {
+4 -1
View File
@@ -617,8 +617,11 @@ export default function MedicationsPageClient() {
"Vollständig bearbeiten" reopens the wizard in edit mode from
there); the list page wizard only ever creates. The wizard owns
its own ResponsiveSheet shell with the sticky footer. */}
{/* v1.36.x — `?new=1` (the retired `/medications/new` route redirects
here) opens the create wizard without passing the gated Add control,
so the wizard asks the same `canAdd` the control asks. */}
<MedicationWizardDialog
open={dialogOpen}
open={dialogOpen && canAdd}
onOpenChange={setDialogOpen}
mode="create"
onSuccess={closeDialog}