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>
63 lines
1.7 KiB
TypeScript
63 lines
1.7 KiB
TypeScript
import { Prisma } from "@prisma/client";
|
|
import { prisma } from "@/lib/db";
|
|
import { slugify } from "@/lib/utils";
|
|
|
|
/**
|
|
* Erzeugt einen je Besitzer eindeutigen Slug, notfalls mit angehaengtem
|
|
* Zaehler. Zwei Benutzer duerfen also beide ein Projekt "dashboard" haben.
|
|
*/
|
|
export async function uniqueSlug(
|
|
name: string,
|
|
ownerId: string,
|
|
ignoreId?: string,
|
|
): Promise<string> {
|
|
const base = slugify(name) || "projekt";
|
|
let candidate = base;
|
|
let counter = 2;
|
|
|
|
// In der Praxis endet die Schleife nach wenigen Runden.
|
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
const existing = await prisma.project.findFirst({
|
|
where: { slug: candidate, ownerId },
|
|
select: { id: true },
|
|
});
|
|
if (!existing || existing.id === ignoreId) return candidate;
|
|
candidate = `${base}-${counter}`;
|
|
counter += 1;
|
|
}
|
|
return `${base}-${Date.now()}`;
|
|
}
|
|
|
|
/**
|
|
* Verbindungsfelder, die an den Browser gehen duerfen.
|
|
* tokenCipher ist bewusst nicht dabei.
|
|
*/
|
|
export const connectionSafeSelect = Prisma.validator<Prisma.GitConnectionSelect>()({
|
|
id: true,
|
|
name: true,
|
|
provider: true,
|
|
baseUrl: true,
|
|
username: true,
|
|
tokenHint: true,
|
|
lastCheckedAt: true,
|
|
lastCheckOk: true,
|
|
lastCheckMessage: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
});
|
|
|
|
export const projectListInclude = Prisma.validator<Prisma.ProjectInclude>()({
|
|
connection: { select: connectionSafeSelect },
|
|
snapshot: true,
|
|
_count: { select: { notes: true } },
|
|
});
|
|
|
|
export const projectDetailInclude = Prisma.validator<Prisma.ProjectInclude>()({
|
|
connection: { select: connectionSafeSelect },
|
|
snapshot: true,
|
|
notes: {
|
|
orderBy: [{ pinned: "desc" }, { updatedAt: "desc" }],
|
|
},
|
|
_count: { select: { notes: true } },
|
|
});
|