Files
Wabenhain/src/lib/flash-core.ts
T
Nick SzaunigandClaude Opus 5.5 839bbff595 feat: sichtbare Rueckmeldung fuer alle Aktionen (Toast statt URL-Meldungen)
Viele Aktionen gaben keine Rueckmeldung (Retoure annehmen, Archiv,
Bewertungen, Adressen, Newsletter, Kundensperre ...) oder meldeten Erfolg,
obwohl die zugehoerige E-Mail nie rausging.

- lib/flash: Meldung als kurzlebiges Cookie; Toast im Root-Layout
  (FlashToast). Ersetzt ?message=/?error= in der URL — darueber liess sich
  bisher beliebiger Text als Systemmeldung im Wabenhain-Layout anzeigen
  (Phishing-Hebel). Server Components leiten ueber /api/hinweis mit festen
  Codes und festen Zielen weiter.
- fail()/succeed() zentral und asynchron; ein Quelltext-Test stellt sicher,
  dass jeder Aufruf mit await/return erfolgt (sonst liefe eine Action nach
  einem Fehler weiter).
- sendMail meldet, ob die Mail angenommen wurde; Aktionen mit Mail zeigen
  ehrlich an, wenn der Versand scheitert (z.B. "Link erneut senden").
- Jede Server Action meldet jetzt Erfolg oder den Grund des Abbruchs.
- SubmitFeedback: geklickter Knopf zeigt sofort einen Ladezustand und ist
  bis zur Antwort gegen Doppelklick gesperrt.
- 2FA: nach dem Einrichten zurueck ins Profil statt auf die Seite mit
  "Deaktivieren".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 04:36:36 +02:00

62 lines
2.0 KiB
TypeScript

/**
* Reine Hilfsfunktionen der Flash-Meldungen (siehe lib/flash.ts) ohne
* next/headers — damit auch der Proxy (src/proxy.ts) und Route Handler sie
* nutzen koennen.
*/
export const FLASH_COOKIE = "wh_flash";
export const MAX_FLASH_MESSAGE_LENGTH = 300;
/** Ohne JavaScript loescht niemand das Cookie — dann verfaellt es von selbst. */
const FLASH_MAX_AGE_SECONDS = 60;
const FLASH_KINDS = ["success", "error", "info"] as const;
export type FlashKind = (typeof FLASH_KINDS)[number];
export interface Flash {
id: string;
kind: FlashKind;
message: string;
}
export function serializeFlash(flash: Flash): string {
return encodeURIComponent(JSON.stringify({ ...flash, message: flash.message.slice(0, MAX_FLASH_MESSAGE_LENGTH) }));
}
export function parseFlash(raw: string | undefined): Flash | null {
if (!raw) return null;
let value: unknown;
try {
value = JSON.parse(decodeURIComponent(raw));
} catch {
return null;
}
if (typeof value !== "object" || value === null) return null;
const { id, kind, message } = value as Record<string, unknown>;
if (typeof id !== "string" || typeof message !== "string" || message.trim() === "") return null;
if (!(FLASH_KINDS as readonly unknown[]).includes(kind)) return null;
return { id: id.slice(0, 64), kind: kind as FlashKind, message: message.slice(0, MAX_FLASH_MESSAGE_LENGTH) };
}
export function flashCookieOptions() {
return {
path: "/",
maxAge: FLASH_MAX_AGE_SECONDS,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
httpOnly: false,
};
}
/**
* Fuer Route Handler, die selbst eine NextResponse zurueckgeben (z.B. der
* Link aus der Bestaetigungs-E-Mail): haengt die Meldung direkt an die
* Weiterleitung.
*/
export function withFlash<T extends { cookies: { set: (name: string, value: string, options: object) => unknown } }>(
response: T,
kind: FlashKind,
message: string
): T {
response.cookies.set(FLASH_COOKIE, serializeFlash({ id: crypto.randomUUID(), kind, message }), flashCookieOptions());
return response;
}