Files
nebula/src/app/api/notes/[id]/route.ts
T
florian.wildeandClaude Opus 5 ed77ffaed2 feat(security): alle Daten auf den Besitzer einschraenken
Bisher sah jede Anfrage alles. Jetzt traegt jede Abfrage eine
Besitzerbedingung, und zwar an erster Stelle in der where-Klausel statt als
nachtraeglicher Vergleich - so kann sie nicht versehentlich uebersprungen
werden.

- Lesen: findFirst statt findUnique, immer mit ownerId
- Schreiben und Loeschen: updateMany/deleteMany mit ownerId in der
  where-Klausel. Fremde Datensaetze werden dadurch gar nicht erst getroffen,
  und die betroffene Zeilenzahl sagt, ob es den Datensatz gab.
- Notizen haengen ueber project.ownerId am Besitzer
- Beim Verknuepfen eines Projekts wird geprueft, dass die Git-Verbindung dem
  Aufrufer gehoert - sonst liesse sich eine fremde Verbindung und damit ein
  fremder Token unterschieben
- Fremde IDs liefern 404 statt 403: die Antwort bestaetigt nicht einmal,
  dass es den Datensatz gibt
- Ausgehende Endpunkte (Sync, Verbindungstest, Repo-Liste) zusaetzlich
  ratenbegrenzt, Basis-URLs gegen den SSRF-Schutz geprueft
- Slug und Verbindungsname werden je Besitzer auf Eindeutigkeit geprueft

Nachgewiesen: ein zweites Konto erhaelt auf GET, PATCH, DELETE, Notiz
anlegen und Sync eines fremden Projekts durchgaengig 404, auf die
Admin-Endpunkte 403, und das Zielprojekt bleibt unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:30:59 +02:00

63 lines
2.0 KiB
TypeScript

import { NextResponse } from "next/server";
import { requireUserApi } from "@/lib/auth/guard";
import { prisma } from "@/lib/db";
import { jsonError, readJsonBody } from "@/lib/security/request";
import { formatZodError, noteUpdateSchema } from "@/lib/validation";
export const dynamic = "force-dynamic";
type Params = { params: Promise<{ id: string }> };
/** Eine Notiz gehoert dem, dem das zugehoerige Projekt gehoert. */
async function ownedNote(noteId: string, ownerId: string) {
return prisma.note.findFirst({
where: { id: noteId, project: { ownerId } },
select: { id: true, projectId: true },
});
}
export async function PATCH(request: Request, { params }: Params) {
const auth = await requireUserApi();
if (!auth.ok) return auth.response;
const { id } = await params;
const note = await ownedNote(id, auth.user.id);
if (!note) return jsonError("Notiz nicht gefunden", 404);
const body = await readJsonBody(request);
if (!body.ok) return body.response;
const parsed = noteUpdateSchema.safeParse(body.data);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validierung fehlgeschlagen", fields: formatZodError(parsed.error) },
{ status: 422 },
);
}
const updated = await prisma.note.update({
where: { id },
data: {
...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),
...(parsed.data.content !== undefined ? { content: parsed.data.content } : {}),
...(parsed.data.pinned !== undefined ? { pinned: parsed.data.pinned } : {}),
},
});
return NextResponse.json({ note: updated });
}
export async function DELETE(_request: Request, { params }: Params) {
const auth = await requireUserApi();
if (!auth.ok) return auth.response;
const { id } = await params;
const result = await prisma.note.deleteMany({
where: { id, project: { ownerId: auth.user.id } },
});
if (result.count === 0) return jsonError("Notiz nicht gefunden", 404);
return NextResponse.json({ ok: true });
}