Devices/Magic: - Offline-Profil-Enroll deaktiviert (410) — Lock-PW würde im Klartext im Download landen; stationärer Schutz läuft jetzt nur über Rebreak Magic - Mac-DNS-Template: ProhibitDisablement (Filter nicht abschaltbar) - Push "Neues Gerät verbunden" an mobile Geräte bei neuer Bindung - Realtime auf user_devices → Settings aktualisiert Magic-Bindings live - Geräte-Detail-Sheet (Tap auf Gerät): Status, verbunden-seit, Schutz-Donut Hard-Lock (server-gehaltenes Removal-PW, User sieht es nie): - magic_removal_password generiert/gespeichert + in Profil injiziert (Lazy-Backfill) - Reveal NUR bei Account-Löschung (user/delete) + Kündigung (stripe webhook), per Resend-Mail + in-Response - Signing config-gated (inaktiv ohne Cert; Lock greift auch unsigniert) Migrations: user_devices-Realtime-Publication + magic_removal_password-Spalten Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
/**
|
|
* Magic Removal-Password Reveal-Email.
|
|
*
|
|
* Wird versendet wenn der User KÜNDIGT oder seinen ACCOUNT LÖSCHT — dann (und
|
|
* nur dann) bekommt er die server-gehaltenen Removal-Passwörter, um die
|
|
* gesperrten DNS-Profile von seinen Mac/Windows-Geräten zu entfernen.
|
|
*
|
|
* Im Normalbetrieb bleibt das Passwort serverseitig — der User sieht es nie
|
|
* (Hard-Lock, gegen impulsives Abschalten). Siehe memory/mac-magic-profile-lock.
|
|
*
|
|
* Fire-and-forget: Fehler werden geloggt, nie geworfen.
|
|
*/
|
|
|
|
import { Resend } from "resend";
|
|
import type { MagicRemovalCredential } from "../db/devices";
|
|
|
|
export interface MagicRemovalEmailOpts {
|
|
recipientEmail: string;
|
|
/** Nickname für die Anrede (NIE firstName/email im Body außer Empfänger). */
|
|
recipientNickname?: string | null;
|
|
credentials: MagicRemovalCredential[];
|
|
reason: "cancellation" | "account_deletion";
|
|
resendApiKey: string;
|
|
}
|
|
|
|
export async function sendMagicRemovalEmail(
|
|
opts: MagicRemovalEmailOpts,
|
|
): Promise<void> {
|
|
if (!opts.resendApiKey) {
|
|
console.warn("[magic-removal-email] resendApiKey not provided — skipping mail");
|
|
return;
|
|
}
|
|
if (opts.credentials.length === 0) return;
|
|
|
|
const resend = new Resend(opts.resendApiKey);
|
|
const greeting = opts.recipientNickname ? `Hallo ${opts.recipientNickname},` : "Hallo,";
|
|
const reasonLine =
|
|
opts.reason === "account_deletion"
|
|
? "Du hast deinen ReBreak-Account gelöscht."
|
|
: "Dein ReBreak-Abo wurde beendet.";
|
|
|
|
const rows = opts.credentials
|
|
.map((c) => {
|
|
const label = c.hostname || c.model || "Computer";
|
|
return `
|
|
<tr>
|
|
<td style="padding:10px 14px;border-bottom:1px solid #f0f0f0;font-size:14px;color:#3a3a3a;">${label}</td>
|
|
<td style="padding:10px 14px;border-bottom:1px solid #f0f0f0;font-size:16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:1px;color:#1a1a1a;"><strong>${c.removalPassword}</strong></td>
|
|
</tr>`;
|
|
})
|
|
.join("");
|
|
|
|
const subject = "Dein Entfern-Passwort für den ReBreak-Schutz";
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${subject}</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a; background: #f5f5f7; margin: 0; padding: 0; }
|
|
.container { max-width: 560px; margin: 32px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
.header { background: #1a1a1a; padding: 24px 32px; }
|
|
.header h1 { color: #fff; font-size: 18px; font-weight: 600; margin: 0; letter-spacing: -0.3px; }
|
|
.body { padding: 28px 32px; }
|
|
.body p { font-size: 15px; line-height: 1.6; color: #3a3a3a; margin: 0 0 16px; }
|
|
table { width: 100%; border-collapse: collapse; margin: 8px 0 20px; }
|
|
.steps { background: #f5f5f7; border-radius: 8px; padding: 14px 18px; font-size: 14px; color: #555; line-height: 1.6; }
|
|
.footer { padding: 16px 32px; font-size: 12px; color: #888; border-top: 1px solid #f0f0f0; }
|
|
</style></head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header"><h1>ReBreak — Schutz entfernen</h1></div>
|
|
<div class="body">
|
|
<p>${greeting}</p>
|
|
<p>${reasonLine} Damit du den ReBreak-Schutz von deinen Geräten entfernen kannst, hier deine Entfern-Passwörter:</p>
|
|
<table>${rows}</table>
|
|
<div class="steps">
|
|
<strong>So entfernst du den Schutz (Mac):</strong><br>
|
|
Systemeinstellungen → Allgemein → Geräteverwaltung → „ReBreak Schutz" → Entfernen → Passwort eingeben.
|
|
</div>
|
|
<p style="margin-top:20px;font-size:13px;color:#888;">Bewahre diese Mail auf, bis du den Schutz entfernt hast. Das Passwort wird aus Sicherheitsgründen nur hier herausgegeben.</p>
|
|
</div>
|
|
<div class="footer">Fragen? support@rebreak.org</div>
|
|
</div>
|
|
</body>
|
|
</html>`.trim();
|
|
|
|
try {
|
|
await resend.emails.send({
|
|
from: "ReBreak <noreply@rebreak.org>",
|
|
to: opts.recipientEmail,
|
|
subject,
|
|
html,
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[magic-removal-email] Failed to send:", err?.message ?? err);
|
|
}
|
|
}
|