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
79 lines
2.0 KiB
TypeScript
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),
|
|
},
|
|
};
|
|
});
|