/** * Device-Lock Email-Notification * * Wird versendet wenn ein fremder Account versucht sich auf einem gebundenen * Gerät (Pro/Legend) einzuloggen. * * Rate-Limit: max. 1 Mail pro Device pro 6h (lockNotifiedAt in UserDevice). * * Anonymität: nur nickname wird im Mail-Body gezeigt — NIEMALS firstName/email. * Siehe memory/feedback_anonymity_nickname.md * * Template-Inhalt ist bewusst sprachneutral (strukturierte Info) und NICHT * Lyra-Voice-formatiert (kein SOS-Ton). Plain informational. */ import { Resend } from "resend"; const LOCK_NOTIFY_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h export interface DeviceLockEmailOpts { /** Nickname des Original-Users (der das Gerät besitzt) */ recipientNickname: string; /** Email-Adresse des Original-Users (Supabase-Auth-Email — nur für Versand, erscheint NICHT im Body) */ recipientEmail: string; /** Row-ID des UserDevice (für Release-Link) */ deviceRowId: string; /** Lesbarer Name des Geräts (z.B. "Chahines iPhone") oder null */ deviceName: string | null; /** lockNotifiedAt des Devices — Rate-Limit-Check */ lockNotifiedAt: Date | null; /** Resend API Key — caller holt via useRuntimeConfig im Request-Context */ resendApiKey: string; /** App-Base-URL für Links (z.B. "https://app.rebreak.org") */ appBaseUrl?: string; } /** * Gibt true zurück wenn innerhalb der letzten 6h bereits eine Mail für dieses * Device verschickt wurde (Rate-Limit-Check ohne DB-Call nötig). */ export function isLockNotifyRateLimited(lockNotifiedAt: Date | null): boolean { if (!lockNotifiedAt) return false; return Date.now() - lockNotifiedAt.getTime() < LOCK_NOTIFY_COOLDOWN_MS; } /** * Sendet die Device-Lock-Notification per Resend. * Fire-and-forget Pattern: Caller sollte nicht auf Fehler warten. * Bei Fehler wird geloggt aber kein Throw (Mail-Fehler blockiert nie den Auth-Flow). */ export async function sendDeviceLockEmail(opts: DeviceLockEmailOpts): Promise { if (!opts.resendApiKey) { console.warn("[device-lock-email] resendApiKey not provided — skipping mail"); return; } const resend = new Resend(opts.resendApiKey); const baseUrl = opts.appBaseUrl ?? "https://app.rebreak.org"; const deviceLabel = opts.deviceName ?? "dein Gerät"; // Deep-Link zur Devices-Settings-Page — Frontend rendert Release-Button const devicesUrl = `${baseUrl}/settings/devices`; const reviewUrl = `${baseUrl}/settings`; const subject = `Anmeldeversuch auf ${deviceLabel}`; const html = ` ${subject}

ReBreak — Sicherheitshinweis

Hallo ${opts.recipientNickname},

Auf ${deviceLabel} hat sich jemand mit einem anderen Account angemeldet. Dein Gerät ist mit deinem ReBreak-Account verknüpft — der Anmeldeversuch wurde geblockt.

Was das bedeutet:
Dein Schutz ist aktiv. Der andere Account hat auf diesem Gerät keinen Zugang bekommen. Falls das du warst und du den Account wechseln möchtest, kannst du das Gerät unten freigeben.

Wenn das nicht du warst: Alles ist in Ordnung — kein Handlungsbedarf. Dein Schutz funktioniert wie gewünscht.

Wenn das du warst: Du kannst das Gerät freigeben. Bitte beachte: die Freigabe wird erst nach 24 Stunden wirksam. Das ist ein bewusstes Sicherheits-Feature — es schützt dich davor, im Drang-Moment impulsiv die Schutz-Bindung aufzuheben.

`.trim(); try { await resend.emails.send({ from: "ReBreak ", to: opts.recipientEmail, subject, html, }); } catch (err: any) { console.error("[device-lock-email] Failed to send:", err?.message ?? err); } }