feat(erinnerungen): mailserver in der app einrichten

SMTP-Server, Port, Verschluesselung, Zugang und Absender unter Mehr ->
Erinnerungen, gespeichert in der neuen Tabelle app_setting. Das Passwort
liegt mit AES-256-GCM verschluesselt (Schluessel aus BETTER_AUTH_SECRET),
wird nie angezeigt und nicht exportiert. Die .env hat weiter Vorrang. Der
Zeitplan laeuft jetzt immer und prueft je Durchlauf, ob ein Server da ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
unknown
2026-10-25 22:40:36 +01:00
co-authored by Claude Opus 5.5
parent ebdd6c313a
commit 01bbb3ba9a
15 changed files with 2731 additions and 44 deletions
@@ -1,9 +1,23 @@
'use server';
import { revalidatePath } from 'next/cache';
import { actionData, actionError, type ActionResult } from '@/lib/action-result';
import {
actionData,
actionError,
actionOk,
formToObject,
parseForm,
type ActionResult,
} from '@/lib/action-result';
import { env } from '@/env';
import {
deleteMailSettings,
getMailConfig,
isMailConfigured,
saveMailSettings,
} from '@/lib/mail-settings';
import { sendMail, verifyMailConnection } from '@/lib/mailer';
import { mailSettingsSchema } from '@/lib/validation';
import { runReminderJob, type ReminderRun } from '@/lib/reminder-job';
import { requireAdmin } from '@/lib/session';
@@ -17,7 +31,7 @@ export async function testMailConnection(): Promise<ActionResult> {
export async function sendTestMail(): Promise<ActionResult> {
const admin = await requireAdmin();
if (!env.isMailConfigured) {
if (!isMailConfigured()) {
return actionError('Es ist kein SMTP-Server konfiguriert.');
}
@@ -41,7 +55,7 @@ export async function sendTestMail(): Promise<ActionResult> {
export async function runRemindersNow(): Promise<ActionResult<ReminderRun>> {
await requireAdmin();
if (!env.isMailConfigured) {
if (!isMailConfigured()) {
return actionError('Es ist kein SMTP-Server konfiguriert.');
}
@@ -49,3 +63,26 @@ export async function runRemindersNow(): Promise<ActionResult<ReminderRun>> {
revalidatePath('/verwaltung/erinnerungen');
return actionData(run);
}
/** Stores the SMTP server set up on the reminders page. */
export async function saveMailSettingsAction(formData: FormData): Promise<ActionResult> {
await requireAdmin();
if (getMailConfig()?.source === 'env') {
return actionError('Der Mailserver kommt aus der .env. Änderungen bitte dort vornehmen.');
}
const parsed = parseForm(mailSettingsSchema, formToObject(formData));
if (!parsed.ok) return parsed;
saveMailSettings(parsed.data);
revalidatePath('/verwaltung/erinnerungen');
return actionOk();
}
export async function deleteMailSettingsAction(): Promise<ActionResult> {
await requireAdmin();
deleteMailSettings();
revalidatePath('/verwaltung/erinnerungen');
return actionOk();
}
+12 -5
View File
@@ -3,10 +3,12 @@ import { eq, sql } from 'drizzle-orm';
import { db } from '@/db';
import { wishlist } from '@/db/schema';
import { env } from '@/env';
import { MailSettingsForm } from '@/components/verwaltung/mail-settings-form';
import {
ReminderSettings,
type ReminderListRow,
} from '@/components/verwaltung/reminder-settings';
import { getMailSettingsView } from '@/lib/mail-settings';
import { daysUntil } from '@/lib/reminders';
import { requireAdmin } from '@/lib/session';
@@ -41,11 +43,16 @@ export default async function ErinnerungenPage() {
}))
.sort((a, b) => (a.daysLeft ?? 0) - (b.daysLeft ?? 0));
const mail = getMailSettingsView();
return (
<ReminderSettings
configured={env.isMailConfigured}
daysBefore={env.REMINDER_DAYS_BEFORE}
lists={lists}
/>
<div className="space-y-6">
<ReminderSettings
configured={mail.source !== null}
daysBefore={env.REMINDER_DAYS_BEFORE}
lists={lists}
/>
<MailSettingsForm view={mail} />
</div>
);
}