chahinebrini 941dd60f36 feat(magic): pairing-code login flow
Backend:
- MagicPairingCode + MagicSession Prisma models
- /api/magic/pair/create (6-digit code, 10min TTL, single-use)
- /api/magic/pair/redeem (no auth, returns mgc_* token)
- /api/magic/info (public DMG metadata)
- requireUser() accepts mgc_* tokens

Mac-App (RebreakMagic):
- LoginView: 6-digit code input (OTP-style), real AppIcon, no signup
- AuthService: signInWithPairingCode() replaces email/pw flow

Native-App:
- MagicSheet (TrueSheet) in Settings: download + code generator + linked Macs
- AddMacSheet: subtle banner pointing to /settings
- de/en locales
2026-06-03 00:18:24 +02:00

77 lines
1.9 KiB
TypeScript

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.
*/
export default defineEventHandler(async (event) => {
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(),
},
};
});