/** * POST /api/users/me/push-token * * Client (Expo) ruft das nach `getExpoPushTokenAsync()` auf, um seinen Token * im Backend zu hinterlegen. Idempotent: bei existierendem Token wird nur * lastUsedAt + enabled aktualisiert. * * Body: { token: string, platform: "ios" | "android", deviceId?: string } */ import { requireUser } from "../../../utils/auth"; import { usePrisma } from "../../../utils/prisma"; import { z } from "zod"; const Body = z.object({ token: z.string().min(10).max(200), // ExponentPushToken[xxx] platform: z.enum(["ios", "android"]), deviceId: z.string().max(120).optional(), /// iOS-PushKit-Token (64-char hex) für CallKit-Wake-Pushes. Optional — /// Client kann später via separatem Call dieselbe Row updaten. voipToken: z.string().min(32).max(200).optional(), }); export default defineEventHandler(async (event) => { const user = await requireUser(event); const raw = await readBody(event).catch(() => ({})); const parsed = Body.safeParse(raw); if (!parsed.success) { throw createError({ statusCode: 400, data: { error: "INVALID_BODY", detail: parsed.error.flatten() }, }); } const { token, platform, deviceId, voipToken } = parsed.data; const db = usePrisma(); console.log( `[push-token] register user=${user.id.slice(0,8)} platform=${platform} ` + `token=${token.slice(0,25)}\u2026 voip=${voipToken ? voipToken.slice(0,16)+'\u2026' : 'none'} ` + `device=${deviceId ?? 'none'}`, ); await db.pushToken.upsert({ where: { token }, create: { userId: user.id, token, platform, deviceId: deviceId ?? null, voipToken: voipToken ?? null, enabled: true, lastUsedAt: new Date(), }, update: { userId: user.id, // Token könnte das Device gewechselt haben platform, deviceId: deviceId ?? null, // Wichtig: voipToken nur überschreiben wenn der Client einen mitliefert, // sonst behalten (separate VoIP-Rotation-Calls könnten ihn schon gesetzt haben). ...(voipToken !== undefined ? { voipToken } : {}), enabled: true, lastUsedAt: new Date(), }, }); return { success: true, data: { ok: true } }; });