heartbeats.post.ts
1 import { PrismaClient } from "@prisma/client"; 2 import { H3Event } from "h3"; 3 import { z } from "zod"; 4 import { handleApiError } from "~~/server/utils/logging"; 5 6 const prisma = new PrismaClient({ 7 log: ['warn', 'error'], 8 }); 9 10 const apiKeySchema = z.string().uuid(); 11 12 const heartbeatSchema = z.object({ 13 timestamp: z.string().datetime().or(z.number()), 14 project: z.string().min(1).max(255), 15 language: z.string().min(1).max(50), 16 editor: z.string().min(1).max(50), 17 os: z.string().min(1).max(50), 18 branch: z.string().max(255).optional(), 19 file: z.string().max(255), 20 }); 21 22 export default defineEventHandler(async (event: H3Event) => { 23 try { 24 const authHeader = getHeader(event, "authorization"); 25 if (!authHeader || !authHeader.startsWith("Bearer ")) { 26 throw handleApiError(401, "Heartbeat API error: Missing or invalid API key format in header."); 27 } 28 29 const apiKey = authHeader.substring(7); 30 const validationResult = apiKeySchema.safeParse(apiKey); 31 32 if (!validationResult.success) { 33 throw handleApiError(401, `Heartbeat API error: Invalid API key format. Key: ${apiKey.substring(0,4)}...`); 34 } 35 36 const user = await prisma.user.findUnique({ 37 where: { apiKey }, 38 select: { id: true, apiKey: true }, 39 }); 40 41 if (!user || user.apiKey !== apiKey) { 42 throw handleApiError(401, `Heartbeat API error: Invalid API key. Key: ${apiKey.substring(0,4)}...`); 43 } 44 45 const body = await readBody(event); 46 const validatedData = heartbeatSchema.parse(body); 47 48 const timestamp = 49 typeof validatedData.timestamp === "number" 50 ? BigInt(validatedData.timestamp) 51 : BigInt(new Date(validatedData.timestamp).getTime()); 52 53 const heartbeat = await prisma.heartbeats.create({ 54 data: { 55 userId: user.id, 56 timestamp, 57 project: validatedData.project, 58 language: validatedData.language, 59 editor: validatedData.editor, 60 os: validatedData.os, 61 branch: validatedData.branch, 62 file: validatedData.file, 63 }, 64 }); 65 66 return { 67 success: true, 68 id: heartbeat.id, 69 }; 70 } catch (error: any) { 71 if (error && typeof error === "object" && error.statusCode) throw error; 72 if (error instanceof z.ZodError) { 73 throw handleApiError(400, `Heartbeat API error: Validation error. Details: ${error.errors[0].message}`); 74 } 75 const detailedMessage = error instanceof Error ? error.message : "An unknown error occurred processing heartbeat."; 76 const apiKeyPrefix = getHeader(event, "authorization")?.substring(7,11) || "UNKNOWN"; 77 throw handleApiError(500, `Heartbeat API error: Failed to process heartbeat. API Key prefix: ${apiKeyPrefix}... Error: ${detailedMessage}`, "Failed to process your request."); 78 } 79 });