fix: recover managed sharing boundaries

This commit is contained in:
Marc-André Bombeck committed 2026-08-06 00:19:35 +02:00
1 parent 01562b4aef
commit d86358f3e4
96 files changed
+3578 -397

No files matched your search

@@ -16,7 +16,7 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
@@ -138,7 +138,11 @@ export const GET = apiHandler(
export const POST = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
// v1.37.0 — MANAGE, and this one is an edit wearing a create's verb: the
// upsert revives a tombstoned day and replaces that day's symptom links.
// Admitted at the level that admits editing, on the condition that the
// audit row says what the write replaced.
const { user } = await requireRecordAuth("manage", "illness");
const gate = await requireIllnessEnabled(user.id);
if (!gate.enabled) return gate.response;
@@ -169,7 +173,18 @@ export const POST = apiHandler(
await auditLog("illness.day-log.upsert", {
userId: user.id,
ipAddress: getClientIp(request),
details: { dayLogId: result.id, episodeId: id, existed: result.existed },
// C3 in this domain's shape: the date, the symptom set that was there
// before, and whether the write brought back a day the owner deleted.
// Without those three the feed says "updated a day log" about a day
// somebody deliberately removed.
details: {
dayLogId: result.id,
episodeId: id,
existed: result.existed,
date: parsed.data.date,
previousSymptoms: result.replaced.previousSymptomKeys,
revived: result.replaced.revived,
},
});
annotate({
@@ -10,7 +10,7 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
@@ -28,7 +28,9 @@ type RouteParams = { params: Promise<{ id: string }> };
export const PATCH = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
// v1.37.0 — MANAGE. Closing an episode; the window it writes is the
// record's own and the reopen is one route over.
const { user } = await requireRecordAuth("manage", "illness");
const gate = await requireIllnessEnabled(user.id);
if (!gate.enabled) return gate.response;
@@ -13,7 +13,7 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
@@ -25,7 +25,9 @@ type RouteParams = { params: Promise<{ id: string }> };
export const POST = apiHandler(
withIdempotency<[NextRequest, RouteParams]>(async (request, { params }) => {
const { user } = await requireAuth();
// v1.37.0 — MANAGE. Reopening a deleted episode with its day logs
// intact; the undo beside a delete the level admits.
const { user } = await requireRecordAuth("manage", "illness");
const gate = await requireIllnessEnabled(user.id);
if (!gate.enabled) return gate.response;
+37 -4
View File
@@ -10,9 +10,10 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { overwriteDetails } from "@/lib/sharing/audit-details";
import {
apiSuccess,
apiError,
@@ -55,7 +56,10 @@ export const GET = apiHandler(
export const PATCH = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
// v1.37.0 — MANAGE. The module gate below resolves against the RECORD, so
// a manager cannot edit an episode in a record whose owner switched the
// module off.
const { user } = await requireRecordAuth("manage", "illness");
const gate = await requireIllnessEnabled(user.id);
if (!gate.enabled) return gate.response;
@@ -69,6 +73,11 @@ export const PATCH = apiHandler(
deletedAt: true,
onsetAt: true,
resolvedAt: true,
// v1.37.0 — the pre-image the audit row carries (C4).
label: true,
type: true,
lifecycle: true,
parentConditionId: true,
},
});
if (
@@ -150,7 +159,29 @@ export const PATCH = apiHandler(
await auditLog("illness.episode.update", {
userId: user.id,
ipAddress: getClientIp(request),
details: { episodeId: id },
// C4 — the replaced fields; the note is named and never quoted.
details: {
episodeId: id,
...overwriteDetails({
before: {
label: existing.label,
type: existing.type,
lifecycle: existing.lifecycle,
onsetAt: existing.onsetAt,
resolvedAt: existing.resolvedAt,
parentConditionId: existing.parentConditionId,
},
after: {
label: updated.label,
type: updated.type,
lifecycle: updated.lifecycle,
onsetAt: updated.onsetAt,
resolvedAt: updated.resolvedAt,
parentConditionId: updated.parentConditionId,
},
redacted: entry.note !== undefined ? ["note"] : [],
}),
},
});
annotate({
@@ -167,7 +198,9 @@ export const PATCH = apiHandler(
export const DELETE = apiHandler(
async (request: NextRequest, { params }: RouteParams) => {
const { user } = await requireAuth();
// v1.37.0 — MANAGE. Soft delete with a restore route beside it; the day
// logs under the episode come back with it.
const { user } = await requireRecordAuth("manage", "illness");
const gate = await requireIllnessEnabled(user.id);
if (!gate.enabled) return gate.response;