Merge branch 'main' into feat/11-typen-je-gruppe
This commit is contained in:
@@ -7,6 +7,7 @@ import { requirePermission, visibleGroupIds } from "@/lib/auth/policy";
|
||||
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { resolveTargetDomain } from "@/lib/domains";
|
||||
import { findStaticDuplicate } from "@/lib/duplicates";
|
||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||
import { requireAvailableQrType } from "@/lib/qr/availability";
|
||||
import { designSchema } from "@/lib/qr/design";
|
||||
@@ -43,8 +44,32 @@ const createSchema = z.object({
|
||||
groupId: z.string().trim().min(1).optional(),
|
||||
/** Unter welcher Domain die Kurz-URL entsteht. */
|
||||
domainId: z.string().trim().min(1).optional(),
|
||||
/**
|
||||
* Eine gemeldete Dublette bewusst in Kauf nehmen. Ohne dieses Häkchen
|
||||
* antwortet das Anlegen mit 409, sobald derselbe Inhalt schon als
|
||||
* statischer Code in der Gruppe liegt.
|
||||
*/
|
||||
allowDuplicate: z.boolean().default(false),
|
||||
});
|
||||
|
||||
/**
|
||||
* Alle Dateien, die zu einem Code gehören.
|
||||
*
|
||||
* Das sind die des Typ-Moduls — ein PDF, ein Bild — **und** ein eigenes Logo
|
||||
* aus der Gestaltung. Wird das vergessen, löst `attachUploads` das Logo beim
|
||||
* nächsten Speichern wieder vom Code und räumt es später als herrenlos weg.
|
||||
*/
|
||||
function dateienDesCodes(
|
||||
qrType: { uploadIds: (data: never) => string[] },
|
||||
data: unknown,
|
||||
design: { logoSource: string | null; logoUploadId: string | null },
|
||||
): string[] {
|
||||
const ids = qrType.uploadIds(data as never);
|
||||
return design.logoSource === "upload" && design.logoUploadId
|
||||
? [...ids, design.logoUploadId]
|
||||
: ids;
|
||||
}
|
||||
|
||||
function isUniqueViolation(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
@@ -141,6 +166,24 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
await assertSlugAvailable(domainId, gewuenscht);
|
||||
}
|
||||
|
||||
// Zweimal derselbe statische Inhalt ergibt zweimal dasselbe Bild. Das ist
|
||||
// erlaubt, aber selten gewollt — deshalb ein Halt mit Rückfrage statt
|
||||
// eines stillen zweiten Eintrags.
|
||||
if (istStatisch && !parsed.data.allowDuplicate) {
|
||||
const dublette = await findStaticDuplicate(groupId, qrType.id, validated.data);
|
||||
if (dublette) {
|
||||
return Response.json(
|
||||
{
|
||||
error: dublette.label
|
||||
? `Diesen Inhalt hast du schon abgelegt — als „${dublette.label}“.`
|
||||
: "Diesen Inhalt hast du schon abgelegt.",
|
||||
duplicate: dublette,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (istStatisch) {
|
||||
const created = await prisma.qrCode.create({
|
||||
data: {
|
||||
@@ -161,7 +204,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
aliases: { select: { slug: true }, orderBy: { createdAt: "desc" } },
|
||||
},
|
||||
});
|
||||
await attachUploads(created.id, qrType.uploadIds(validated.data));
|
||||
await attachUploads(created.id, dateienDesCodes(qrType, validated.data, design));
|
||||
return Response.json({ code: toQrCodeDto(created) }, { status: 201 });
|
||||
}
|
||||
|
||||
@@ -188,7 +231,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
});
|
||||
// Erst jetzt gehören die hochgeladenen Dateien zu diesem Code —
|
||||
// vorher lagen sie ohne Bezug herum.
|
||||
await attachUploads(created.id, qrType.uploadIds(validated.data));
|
||||
await attachUploads(created.id, dateienDesCodes(qrType, validated.data, design));
|
||||
|
||||
return Response.json({ code: toQrCodeDto(created) }, { status: 201 });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user