Merge branch 'main' into feat/8-logo-im-code
# Conflicts: # docs/funktionen.md
This commit is contained in:
@@ -27,9 +27,9 @@ export default async function MyCodesPage() {
|
||||
Meine Codes
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Alle dynamischen QR-Codes mit ihrem aktuellen Ziel und der Anzahl der
|
||||
Scans. Statische Codes erscheinen hier nicht — sie werden nicht
|
||||
gespeichert.
|
||||
Dynamische Codes mit ihrem aktuellen Ziel und der Anzahl der Scans,
|
||||
dazu abgelegte statische Codes. Über die Auswahl „Nach Art filtern“
|
||||
lässt sich die Liste auf eine der beiden Arten einschränken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getActor } from "@/lib/auth/actor";
|
||||
import { requirePermission } from "@/lib/auth/policy";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { pruefeDomain } from "@/lib/domain-check";
|
||||
import { errorResponse } from "@/lib/http";
|
||||
import { QrRequestError } from "@/lib/qr/type-module";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Context = { params: Promise<{ id: string }> };
|
||||
|
||||
/**
|
||||
* Prüft eine registrierte Domain: DNS, Zertifikat, Erreichbarkeit — und ob
|
||||
* dahinter wirklich diese Installation steht.
|
||||
*
|
||||
* `POST`, obwohl nichts gespeichert wird: Der Aufruf stößt eine Verbindung
|
||||
* nach außen an und ist nichts, was sich vorsorglich abrufen lässt.
|
||||
*
|
||||
* Geprüft wird ausschließlich der Hostname aus der Datenbank. Ein Hostname
|
||||
* aus der Anfrage würde diese Route zum Werkzeug machen, mit dem sich vom
|
||||
* Server aus beliebige Adressen abklopfen lassen.
|
||||
*/
|
||||
export async function POST(
|
||||
_request: Request,
|
||||
context: Context,
|
||||
): Promise<Response> {
|
||||
try {
|
||||
requirePermission(await getActor(), "instance:manage");
|
||||
const { id } = await context.params;
|
||||
|
||||
const domain = await prisma.domain.findUnique({
|
||||
where: { id },
|
||||
select: { hostname: true },
|
||||
});
|
||||
if (!domain) {
|
||||
throw new QrRequestError("Diese Domain gibt es nicht.", 404);
|
||||
}
|
||||
|
||||
return Response.json({ report: await pruefeDomain(domain.hostname) });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
@@ -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,6 +44,12 @@ 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),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -158,6 +165,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: {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { bildeBeweis, getCheckSecret } from "@/lib/domain-check";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Beweis-Route für den Domain-Check.
|
||||
*
|
||||
* Bewusst **ohne Anmeldung**: Der Aufruf kommt von außen über die zu prüfende
|
||||
* Domain und trägt keine Sitzung mit sich. Preisgegeben wird dabei nichts —
|
||||
* die Antwort ist ein HMAC über die mitgeschickte Zufallszahl. Wer das
|
||||
* Geheimnis nicht kennt, kann ihn nicht bilden; wer ihn sieht, lernt daraus
|
||||
* nichts über das Geheimnis.
|
||||
*
|
||||
* Dass es diese Anwendung gibt, verrät ohnehin schon jede Landingpage.
|
||||
*/
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
const nonce = request.nextUrl.searchParams.get("nonce") ?? "";
|
||||
|
||||
// Eine Grenze nach oben, damit die Route nicht als Rechenknecht für fremde
|
||||
// Daten taugt, und eine nach unten, damit sie nicht auf Zuruf antwortet.
|
||||
if (!/^[a-f0-9]{16,64}$/.test(nonce)) {
|
||||
return Response.json(
|
||||
{ error: "Es fehlt eine gültige Zufallszahl." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const proof = bildeBeweis(await getCheckSecret(), nonce);
|
||||
return Response.json(
|
||||
{ app: "qrcode-generator", proof },
|
||||
{ headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Domain-Check konnte keinen Beweis bilden:", error);
|
||||
return Response.json({ error: "Nicht möglich." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user