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

79 lines
2.0 KiB
TypeScript

import { randomInt } from 'crypto';
import { requireUser } from '../../../utils/auth';
/**
* POST /api/magic/pair/create
*
* Native-App ruft auf (Supabase-Auth). Generiert einen 6-stelligen numerischen
* Code mit 10min Lebenszeit. Mac-App tauscht den Code via /pair/redeem gegen
* einen MagicSession-Token.
*
* Returns: { code: "482913", expiresAt: ISO, expiresInSeconds: 600 }
*/
const CODE_TTL_MS = 10 * 60 * 1000; // 10 Minuten
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const db = usePrisma();
// Alte unbenutzte Codes des Users invalidieren (max 1 aktiv pro User)
await db.magicPairingCode.deleteMany({
where: {
userId: user.id,
redeemedAt: null,
},
});
// Generiere unique 6-digit Code (sehr unwahrscheinlich dass dieselbe
// Zahl gleichzeitig aktiv ist, aber wir retry-en sicherheitshalber).
let code: string | null = null;
let attempts = 0;
while (attempts < 5 && code === null) {
const candidate = String(randomInt(0, 1_000_000)).padStart(6, '0');
const exists = await db.magicPairingCode.findUnique({
where: { code: candidate },
select: { id: true, expiresAt: true, redeemedAt: true },
});
if (
!exists ||
exists.redeemedAt !== null ||
exists.expiresAt < new Date()
) {
// Falls expired/redeemed: löschen damit Unique-Constraint frei wird
if (exists) {
await db.magicPairingCode
.delete({ where: { id: exists.id } })
.catch(() => {});
}
code = candidate;
}
attempts++;
}
if (!code) {
throw createError({
statusCode: 500,
message: 'Konnte keinen freien Pairing-Code generieren',
});
}
const expiresAt = new Date(Date.now() + CODE_TTL_MS);
await db.magicPairingCode.create({
data: {
userId: user.id,
code,
expiresAt,
},
});
return {
success: true,
data: {
code,
expiresAt: expiresAt.toISOString(),
expiresInSeconds: Math.floor(CODE_TTL_MS / 1000),
},
};
});