L6 Benachrichtigungen & Audit: Glocke, Posteingang, Nutzer-Einstellungen, Mailkonfiguration
- Glocke im Backoffice-Header (ungelesen-Zähler, letzte 10, alle gelesen), mobil einbindbar über variant="mobile". - /notifications mit Filter gelesen/ungelesen/Art, Öffnen markiert gelesen (nur relative Links), Pagination. - /account Abschnitt Benachrichtigungen: E-Mail-Opt-out je Typ, Notdienst Pflicht. - /settings/email (tenant:manage): Absendername, Antwortadresse, Empfänger Notdienst und Abrechnung; Validierung gegen Header-Injection, max. 20 Adressen, Audit. - Actions unter actions/notifications mit moduleGuard + guard; Navigation ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ctxFromGuard, ServiceError } from "@/server/services/context";
|
||||
import { markAllRead, markRead } from "@/server/services/notifications/inbox";
|
||||
|
||||
const guard = moduleGuard("notifications");
|
||||
|
||||
/** Mark one own notification as read. */
|
||||
export async function markNotificationRead(formData: FormData) {
|
||||
const g = await guard("notification:read");
|
||||
await markRead(ctxFromGuard(g), formData.get("id"));
|
||||
revalidatePath("/", "layout");
|
||||
}
|
||||
|
||||
/** Mark as read and open the linked entity (relative links only). */
|
||||
export async function openNotification(formData: FormData) {
|
||||
const g = await guard("notification:read");
|
||||
let target = "/notifications";
|
||||
try {
|
||||
const { link } = await markRead(ctxFromGuard(g), formData.get("id"));
|
||||
if (link) target = link;
|
||||
} catch (err) {
|
||||
if (!(err instanceof ServiceError)) throw err;
|
||||
}
|
||||
revalidatePath("/", "layout");
|
||||
redirect(target);
|
||||
}
|
||||
|
||||
export async function markAllNotificationsRead() {
|
||||
const g = await guard("notification:read");
|
||||
await markAllRead(ctxFromGuard(g));
|
||||
revalidatePath("/", "layout");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ZodError } from "zod";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import { updateMailSettings } from "@/server/services/notifications/mail-settings";
|
||||
|
||||
const guard = moduleGuard("notifications");
|
||||
|
||||
/** Tenant mail settings (§33.2) — tenant:manage. Result is reported via query string. */
|
||||
export async function saveMailSettings(formData: FormData) {
|
||||
const g = await guard("tenant:manage");
|
||||
let target = "/settings/email?saved=1";
|
||||
try {
|
||||
await updateMailSettings(ctxFromGuard(g), {
|
||||
mailFromName: String(formData.get("mailFromName") ?? ""),
|
||||
mailReplyTo: String(formData.get("mailReplyTo") ?? ""),
|
||||
emergencyRecipients: String(formData.get("emergencyRecipients") ?? ""),
|
||||
billingRecipients: String(formData.get("billingRecipients") ?? ""),
|
||||
});
|
||||
} catch (err) {
|
||||
if (!(err instanceof ZodError)) throw err;
|
||||
const fields = [...new Set(err.issues.map((i) => String(i.path[0] ?? "")))].filter(Boolean).join(",");
|
||||
target = `/settings/email?error=${encodeURIComponent(fields || "input")}`;
|
||||
}
|
||||
revalidatePath("/settings/email");
|
||||
redirect(target);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { ctxFromGuard } from "@/server/services/context";
|
||||
import { setPreferences } from "@/server/services/notifications/preferences";
|
||||
|
||||
const guard = moduleGuard("notifications");
|
||||
|
||||
export type PreferencesState = { status: "idle" | "saved" | "error" };
|
||||
|
||||
/** Save the current user's e-mail opt-outs (checked boxes = e-mail on). */
|
||||
export async function saveNotificationPreferences(_prev: PreferencesState, formData: FormData): Promise<PreferencesState> {
|
||||
const g = await guard("notification:read");
|
||||
try {
|
||||
await setPreferences(ctxFromGuard(g), { emailOn: formData.getAll("emailOn").map(String) });
|
||||
} catch (err) {
|
||||
console.error("[notifications] saving preferences failed:", (err as Error).message);
|
||||
return { status: "error" };
|
||||
}
|
||||
revalidatePath("/account");
|
||||
return { status: "saved" };
|
||||
}
|
||||
Reference in New Issue
Block a user