import { getProfile } from "../../db/profile"; import { findActiveDeviceLock, bindDeviceToUser, isLockingPlan, markDeviceLockNotified, } from "../../db/devices"; import { isLockNotifyRateLimited, sendDeviceLockEmail, } from "../../utils/device-lock-email"; export default defineEventHandler(async (event) => { const { username, password } = await readBody(event); if (!username || !password) { throw createError({ statusCode: 400, message: "username und password erforderlich", }); } // ─── Device-Lock-Check (vor Auth) ────────────────────────────────────────── // Wenn x-device-id Header gesetzt und das Device an einen anderen Pro/Legend- // User gebunden ist → 409 DEVICE_LOCKED. Login wird NICHT durchgeführt. // // Warum vor Auth: wir brauchen nicht zu wissen ob die Credentials korrekt sind. // Das würde nur Timing-Side-Channel für Account-Enumeration öffnen. const incomingDeviceId = getHeader(event, "x-device-id"); if (incomingDeviceId) { // Wir brauchen die user-id des einlogenden Users für den Check — // aber wir haben noch keinen eingeloggten User. Wir übergeben eine // Dummy-UUID die nie matcht, da wir hier nur prüfen ob das Device an // irgendwen (außer NULL) gebunden ist. findActiveDeviceLock matched // "deviceId + NOT userId", also matcht die Dummy-UUID nie auf eine Row. const DUMMY_REQUESTING_USER = "00000000-0000-0000-0000-000000000000"; const lock = await findActiveDeviceLock(incomingDeviceId, DUMMY_REQUESTING_USER); if (lock) { // Async: Mail-Notification an Original-User (Rate-Limited auf 6h) if (!isLockNotifyRateLimited(lock.lockNotifiedAt)) { // Original-User-Profil laden für Mail-Details // Alle Ressourcen hier im äußeren Scope cachen (kein async-after-response) const config = useRuntimeConfig(event); const supabaseCfg = (config as any).public?.supabase ?? (config as any).supabase; const resendApiKey = (config as any).resendApiKey as string | undefined; const supabaseServiceKey = (config as any).supabaseServiceKey as string | undefined; void (async () => { try { const { getProfile: gp } = await import("../../db/profile"); const ownerProfile = await gp(lock.userId); if (ownerProfile && resendApiKey && supabaseServiceKey) { const { createClient } = await import("@supabase/supabase-js"); const adminClient = createClient( supabaseCfg.url as string, supabaseServiceKey, ); const { data: adminUser } = await adminClient.auth.admin.getUserById(lock.userId); const ownerEmail = adminUser?.user?.email; if (ownerEmail) { await sendDeviceLockEmail({ recipientNickname: ownerProfile.nickname ?? ownerProfile.username ?? "Nutzer", recipientEmail: ownerEmail, deviceRowId: lock.id, deviceName: lock.name, lockNotifiedAt: lock.lockNotifiedAt, resendApiKey, }); await markDeviceLockNotified(lock.id); } } } catch (mailErr: any) { console.error("[login] device-lock mail failed:", mailErr?.message ?? mailErr); } })(); } // lockedUntil: release_requested_at + 24h, oder "unbestimmt" wenn kein Request const lockedUntil = lock.releaseRequestedAt ? new Date(lock.releaseRequestedAt.getTime() + 24 * 60 * 60 * 1000).toISOString() : null; throw createError({ statusCode: 409, statusMessage: "DEVICE_LOCKED", data: { error: "DEVICE_LOCKED", lockedUntil, releaseRequestable: true, }, }); } } // ─── Normaler Auth-Flow ───────────────────────────────────────────────────── const email = `${username.toLowerCase()}@rebreak.internal`; const supabase = serverSupabaseClient(event); const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) throw createError({ statusCode: 401, message: error.message }); const dbProfile = await getProfile(data.user.id); const normalizedPlan = ( dbProfile?.plan === "premium" ? "legend" : dbProfile?.plan === "standard" ? "pro" : dbProfile?.plan ?? "free" ) as "free" | "pro" | "legend"; // ─── Device-Binding nach erfolgreichem Login ──────────────────────────────── // Wenn Pro/Legend-User einloggt und Device-ID bekannt → Device binden. // Free-User binden nicht (isLockingPlan filtert). if (incomingDeviceId && isLockingPlan(dbProfile?.plan ?? "free")) { // Fire-and-forget — Login soll nicht wegen Binding-Fehler blockieren void bindDeviceToUser(data.user.id, incomingDeviceId, dbProfile?.plan ?? "free") .catch((err: any) => { console.error("[login] device-bind failed:", err?.message ?? err); }); } return { session: { access_token: data.session.access_token, refresh_token: data.session.refresh_token, expires_at: data.session.expires_at, }, profile: { id: data.user.id, email: data.user.email ?? "", username: dbProfile?.username ?? "", nickname: dbProfile?.nickname ?? null, avatar: dbProfile?.avatar ?? null, plan: normalizedPlan, streak: dbProfile?.streak ?? 0, created_at: dbProfile?.createdAt?.toISOString() ?? data.user.created_at, }, }; });