Verwaltung: QR-Typen einzeln freigeben oder ausblenden
Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete Typen verschwinden vollstaendig von der Startseite und lassen sich nicht mehr neu anlegen. - Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit eine frische Installation ohne Vorbelegung laeuft und spaetere Typen automatisch erscheinen. - Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch, ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich die Freigabe aus der Datenbank (src/lib/qr/availability.ts). - Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein gedruckter Code darf durch einen Schalter nicht ungueltig werden. - resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts. - Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung. - Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getQrType, qrTypeList } from "@/lib/qr/registry";
|
||||
import {
|
||||
isQrTypeId,
|
||||
QrRequestError,
|
||||
type QrTypeId,
|
||||
type QrTypeModule,
|
||||
} from "@/lib/qr/type-module";
|
||||
|
||||
/**
|
||||
* Freigabe der QR-Typen durch die Verwaltung.
|
||||
*
|
||||
* Zwei voneinander unabhängige Eigenschaften bestimmen, was ein Typ kann:
|
||||
*
|
||||
* - `implemented` — steht in der Registry und sagt, ob der Typ überhaupt
|
||||
* ausprogrammiert ist. In V1 gilt das nur für „Website“.
|
||||
* - `enabled` — kommt aus der Datenbank und sagt, ob die Verwaltung den Typ
|
||||
* freigegeben hat. Fehlt der Eintrag, ist der Typ freigegeben.
|
||||
*
|
||||
* Ein abgeschalteter Typ verschwindet aus der Oberfläche und lässt sich nicht
|
||||
* mehr neu anlegen. **Bereits bestehende Codes dieses Typs funktionieren
|
||||
* weiter** — sie sind gedruckt und verteilt, und ein Schalter in der
|
||||
* Verwaltung darf sie nicht ins Leere laufen lassen.
|
||||
*/
|
||||
|
||||
export type QrTypeAvailability = {
|
||||
qrType: QrTypeModule;
|
||||
/** Der Typ ist in dieser Version ausprogrammiert. */
|
||||
implemented: boolean;
|
||||
/** Die Verwaltung hat den Typ freigegeben. */
|
||||
enabled: boolean;
|
||||
/** Freigegeben und ausprogrammiert — nur dann ist er benutzbar. */
|
||||
usable: boolean;
|
||||
/** Freigegeben, aber noch nicht ausprogrammiert — „Bald verfügbar“. */
|
||||
announced: boolean;
|
||||
};
|
||||
|
||||
/** Abschaltungen aus der Datenbank; alles nicht Genannte ist freigegeben. */
|
||||
async function loadDisabledTypes(): Promise<ReadonlySet<string>> {
|
||||
try {
|
||||
const rows = await prisma.qrTypeSetting.findMany({
|
||||
where: { enabled: false },
|
||||
select: { type: true },
|
||||
});
|
||||
return new Set(rows.map((row) => row.type));
|
||||
} catch (error) {
|
||||
// Ohne Datenbank ist die Anwendung für statische Codes weiterhin
|
||||
// brauchbar. Lieber alles anzeigen als eine leere Startseite.
|
||||
console.error(
|
||||
"Freigaben der QR-Typen konnten nicht geladen werden — es gilt die Voreinstellung (alle freigegeben):",
|
||||
error,
|
||||
);
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
/** Verfügbarkeit aller Typen in Anzeigereihenfolge. */
|
||||
export async function getQrTypeAvailability(): Promise<QrTypeAvailability[]> {
|
||||
const disabled = await loadDisabledTypes();
|
||||
|
||||
return qrTypeList.map((qrType) => {
|
||||
const enabled = !disabled.has(qrType.id);
|
||||
return {
|
||||
qrType,
|
||||
implemented: qrType.implemented,
|
||||
enabled,
|
||||
usable: enabled && qrType.implemented,
|
||||
announced: enabled && !qrType.implemented,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Nur die Typen, die in der Oberfläche erscheinen sollen. */
|
||||
export async function getVisibleQrTypes(): Promise<QrTypeAvailability[]> {
|
||||
return (await getQrTypeAvailability()).filter((entry) => entry.enabled);
|
||||
}
|
||||
|
||||
export async function getQrTypeState(
|
||||
id: string,
|
||||
): Promise<QrTypeAvailability | undefined> {
|
||||
if (!isQrTypeId(id)) return undefined;
|
||||
return (await getQrTypeAvailability()).find((entry) => entry.qrType.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Typ für das Anlegen neuer Codes — oder wirft mit einer
|
||||
* Meldung, die unverändert in die Oberfläche gehen kann.
|
||||
*/
|
||||
export async function requireAvailableQrType(
|
||||
id: string,
|
||||
): Promise<QrTypeModule> {
|
||||
const state = await getQrTypeState(id);
|
||||
|
||||
if (!state) {
|
||||
throw new QrRequestError("Unbekannter QR-Typ.", 404);
|
||||
}
|
||||
if (!state.implemented) {
|
||||
throw new QrRequestError(
|
||||
`Der QR-Typ „${state.qrType.title}“ ist in dieser Version noch nicht verfügbar.`,
|
||||
404,
|
||||
);
|
||||
}
|
||||
if (!state.enabled) {
|
||||
throw new QrRequestError(
|
||||
`Der QR-Typ „${state.qrType.title}“ ist in der Verwaltung abgeschaltet.`,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
return state.qrType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typ eines bestehenden Codes. Hier zählt nur, ob der Typ ausprogrammiert
|
||||
* ist — eine Abschaltung in der Verwaltung darf bestehende Codes nicht
|
||||
* unbearbeitbar machen.
|
||||
*/
|
||||
export function requireImplementedQrType(id: string): QrTypeModule {
|
||||
const qrType = getQrType(id);
|
||||
if (!qrType?.implemented) {
|
||||
throw new QrRequestError(
|
||||
"Der Typ dieses Codes wird in dieser Version nicht unterstützt.",
|
||||
404,
|
||||
);
|
||||
}
|
||||
return qrType;
|
||||
}
|
||||
|
||||
/** Freigabe eines Typs setzen. */
|
||||
export async function setQrTypeEnabled(
|
||||
id: QrTypeId,
|
||||
enabled: boolean,
|
||||
): Promise<void> {
|
||||
await prisma.qrTypeSetting.upsert({
|
||||
where: { type: id },
|
||||
create: { type: id, enabled },
|
||||
update: { enabled },
|
||||
});
|
||||
}
|
||||
@@ -120,10 +120,16 @@ export function getQrType(id: string): QrTypeModule | undefined {
|
||||
return isQrTypeId(id) ? qrTypes[id] : undefined;
|
||||
}
|
||||
|
||||
/** Liefert den Typ nur, wenn er in dieser Version nutzbar ist. */
|
||||
export function getActiveQrType(id: string): QrTypeModule | undefined {
|
||||
/**
|
||||
* Liefert den Typ nur, wenn er in dieser Version ausprogrammiert ist.
|
||||
*
|
||||
* Achtung: das ist noch keine Freigabe. Ob ein Typ neu angelegt werden darf,
|
||||
* entscheidet zusätzlich die Verwaltung — dafür `requireAvailableQrType` aus
|
||||
* `src/lib/qr/availability.ts` verwenden.
|
||||
*/
|
||||
export function getImplementedQrType(id: string): QrTypeModule | undefined {
|
||||
const qrType = getQrType(id);
|
||||
return qrType?.active ? qrType : undefined;
|
||||
return qrType?.implemented ? qrType : undefined;
|
||||
}
|
||||
|
||||
export { QR_TYPE_IDS, isQrTypeId };
|
||||
|
||||
+15
-19
@@ -1,8 +1,14 @@
|
||||
import "server-only";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { requireAvailableQrType } from "@/lib/qr/availability";
|
||||
import { designSchema } from "@/lib/qr/design";
|
||||
import { getActiveQrType } from "@/lib/qr/registry";
|
||||
import { QR_TYPE_IDS, type QrTypeModule } from "@/lib/qr/type-module";
|
||||
import {
|
||||
QR_TYPE_IDS,
|
||||
QrRequestError,
|
||||
type QrTypeModule,
|
||||
} from "@/lib/qr/type-module";
|
||||
import { shortUrl } from "@/lib/env";
|
||||
import { isValidSlug } from "@/lib/slug";
|
||||
|
||||
@@ -30,15 +36,9 @@ export const qrRequestSchema = z.object({
|
||||
|
||||
export type QrRequest = z.infer<typeof qrRequestSchema>;
|
||||
|
||||
/** Fehler mit einer Meldung, die unverändert an die Oberfläche geht. */
|
||||
export class QrRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number = 400,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
// Liegt in type-module.ts, damit auch availability.ts ihn werfen kann, ohne
|
||||
// dass ein Ringschluss zwischen den beiden Modulen entsteht.
|
||||
export { QrRequestError };
|
||||
|
||||
export type ResolvedQrRequest = {
|
||||
qrType: QrTypeModule;
|
||||
@@ -51,14 +51,10 @@ export type ResolvedQrRequest = {
|
||||
* Prüft eine Anfrage vollständig und liefert den zu kodierenden Inhalt:
|
||||
* bei statischen Codes die Nutzdaten selbst, bei dynamischen die Kurz-URL.
|
||||
*/
|
||||
export function resolveQrRequest(request: QrRequest): ResolvedQrRequest {
|
||||
const qrType = getActiveQrType(request.type);
|
||||
if (!qrType) {
|
||||
throw new QrRequestError(
|
||||
"Dieser QR-Typ ist in dieser Version noch nicht verfügbar.",
|
||||
404,
|
||||
);
|
||||
}
|
||||
export async function resolveQrRequest(
|
||||
request: QrRequest,
|
||||
): Promise<ResolvedQrRequest> {
|
||||
const qrType = await requireAvailableQrType(request.type);
|
||||
|
||||
const validated = qrType.validate(request.data);
|
||||
if (!validated.ok) {
|
||||
|
||||
@@ -46,6 +46,19 @@ export type ValidationResult =
|
||||
*/
|
||||
export class QrTypeError extends Error {}
|
||||
|
||||
/**
|
||||
* Fehler mit einer Meldung, die unverändert an die Oberfläche geht, plus
|
||||
* dem passenden HTTP-Status.
|
||||
*/
|
||||
export class QrRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number = 400,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Schnittstelle, über die der Rest der Anwendung mit einem QR-Typ
|
||||
* arbeitet. Die konkreten Datentypen sind hier bewusst gelöscht: die
|
||||
@@ -57,8 +70,14 @@ export interface QrTypeModule {
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly icon: IconName;
|
||||
/** Ist der Typ in dieser Version nutzbar? */
|
||||
readonly active: boolean;
|
||||
/**
|
||||
* Ist der Typ in dieser Version ausprogrammiert?
|
||||
*
|
||||
* Nicht zu verwechseln mit der Freigabe: ob ein Typ in der Oberfläche
|
||||
* erscheint, entscheidet zusätzlich die Verwaltung — siehe
|
||||
* `src/lib/qr/availability.ts`.
|
||||
*/
|
||||
readonly implemented: boolean;
|
||||
/** Unterstützt der Typ dynamische Codes (Kurz-URL + Weiterleitung)? */
|
||||
readonly supportsDynamic: boolean;
|
||||
/** Startwerte für ein leeres Formular. */
|
||||
@@ -78,7 +97,7 @@ type QrTypeSpec<TSchema extends z.ZodType> = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: IconName;
|
||||
active: boolean;
|
||||
implemented: boolean;
|
||||
supportsDynamic: boolean;
|
||||
schema: TSchema;
|
||||
defaultData: QrFormData;
|
||||
@@ -120,7 +139,7 @@ export function defineQrType<TSchema extends z.ZodType>(
|
||||
title: spec.title,
|
||||
description: spec.description,
|
||||
icon: spec.icon,
|
||||
active: spec.active,
|
||||
implemented: spec.implemented,
|
||||
supportsDynamic: spec.supportsDynamic,
|
||||
defaultData: spec.defaultData,
|
||||
validate(input) {
|
||||
@@ -157,7 +176,7 @@ export function defineComingSoonType(spec: {
|
||||
title: spec.title,
|
||||
description: spec.description,
|
||||
icon: spec.icon,
|
||||
active: false,
|
||||
implemented: false,
|
||||
supportsDynamic: false,
|
||||
defaultData: {},
|
||||
validate: () => ({
|
||||
|
||||
@@ -47,7 +47,7 @@ export const websiteType = defineQrType({
|
||||
title: "Website",
|
||||
description: "Verlinke eine beliebige Webadresse.",
|
||||
icon: "globe",
|
||||
active: true,
|
||||
implemented: true,
|
||||
supportsDynamic: true,
|
||||
schema: websiteDataSchema,
|
||||
defaultData: { url: "" },
|
||||
|
||||
Reference in New Issue
Block a user