import { randomBytes } from "crypto"; /** * POST /api/magic/pair/redeem * * KEIN auth required — Mac-App hat noch keinen Token. * Body: { code: "482913", label?: "MacBook Pro" } * * Tauscht einen 6-stelligen Pairing-Code (single-use, 10min TTL) gegen einen * MagicSession-Token ("mgc_<48 char>"). Token wird in Mac-Keychain gespeichert * und ersetzt Supabase-JWT für alle /api/magic/* Endpoints. * * Rate-Limit: max. 10 Requests pro IP pro 60s (In-Memory, Nitro Single-Process). */ // In-Memory Rate-Limit-Store. Key = IP, Value = { count, windowStart (ms) }. // Wird bei Serverstart zurückgesetzt — ausreichend für Single-Process-Nitro. const RATE_WINDOW_MS = 60_000; // 1 Minute const RATE_MAX_REQUESTS = 10; interface RateEntry { count: number; windowStart: number; } const rateLimitStore = new Map(); function checkRateLimit(ip: string): boolean { const now = Date.now(); const entry = rateLimitStore.get(ip); if (!entry || now - entry.windowStart >= RATE_WINDOW_MS) { // Neues Fenster starten rateLimitStore.set(ip, { count: 1, windowStart: now }); return true; // erlaubt } if (entry.count >= RATE_MAX_REQUESTS) { return false; // geblockt } entry.count += 1; return true; // erlaubt } export default defineEventHandler(async (event) => { // Rate-Limit prüfen (vor jeder anderen Logik) const ip = getRequestHeader(event, "x-forwarded-for")?.split(",")[0].trim() || getRequestHeader(event, "x-real-ip") || event.node.req.socket?.remoteAddress || "unknown"; if (!checkRateLimit(ip)) { throw createError({ statusCode: 429, message: "Zu viele Versuche. Bitte warte eine Minute.", }); } const body = await readBody(event); const { code, label } = body as { code?: string; label?: string }; if (!code || !/^\d{6}$/.test(code)) { throw createError({ statusCode: 400, message: "code muss 6 Ziffern enthalten", }); } const db = usePrisma(); const pairingCode = await db.magicPairingCode.findUnique({ where: { code }, select: { id: true, userId: true, expiresAt: true, redeemedAt: true, }, }); if (!pairingCode) { throw createError({ statusCode: 404, message: "Code ungültig" }); } if (pairingCode.redeemedAt !== null) { throw createError({ statusCode: 410, message: "Code bereits verwendet" }); } if (pairingCode.expiresAt < new Date()) { throw createError({ statusCode: 410, message: "Code abgelaufen" }); } // Generiere Session-Token const token = "mgc_" + randomBytes(36).toString("base64url"); const session = await db.magicSession.create({ data: { userId: pairingCode.userId, token, label: label?.trim() || null, }, select: { id: true, createdAt: true }, }); // Code als redeemed markieren (single-use) await db.magicPairingCode.update({ where: { id: pairingCode.id }, data: { redeemedAt: new Date(), sessionId: session.id, }, }); return { success: true, data: { token, sessionId: session.id, createdAt: session.createdAt.toISOString(), }, }; });