Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 24 additions & 26 deletions src/app/api/performance/vitals/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { edgeLog } from '@/../infra/edge-config';
import { query } from '@/lib/db/pool';
import { requireAuth } from '@/lib/authMiddleware';

export const runtime = 'nodejs';

Expand All @@ -14,35 +16,23 @@ const RANGE_INTERVALS: Record<string, string> = {
const POOR_ALERT_THRESHOLD_PCT = 5;
const POOR_CHECK_SESSION_LIMIT = 500;

interface VitalsBody {
name: string;
value: number;
rating: string;
url?: string;
timestamp?: number;
id?: string;
delta?: number;
navigationType?: string;
userAgent?: string;
}
const vitalsSchema = z.object({
name: z.string().min(1),
value: z.number().finite(),
rating: z.enum(['good', 'needs-improvement', 'poor']),
url: z.string().optional(),
timestamp: z.number().optional(),
id: z.string().optional(),
delta: z.number().optional(),
navigationType: z.string().optional(),
userAgent: z.string().optional(),
});

function parseRange(range: string | null): string {
if (!range || !RANGE_INTERVALS[range]) return RANGE_INTERVALS['7d'];
return RANGE_INTERVALS[range];
}

function validateMetric(body: unknown): body is VitalsBody {
if (!body || typeof body !== 'object') return false;
const m = body as Record<string, unknown>;
return (
typeof m.name === 'string' &&
typeof m.value === 'number' &&
Number.isFinite(m.value) &&
typeof m.rating === 'string' &&
['good', 'needs-improvement', 'poor'].includes(m.rating)
);
}

async function checkPoorRate(name: string): Promise<void> {
const result = await query(
`
Expand Down Expand Up @@ -75,18 +65,26 @@ async function checkPoorRate(name: string): Promise<void> {
}

export async function POST(request: NextRequest) {
const unauth = requireAuth(request);
if (unauth) return unauth;

edgeLog('info', '/api/performance/vitals', 'POST request received');
try {
const body: unknown = await request.json();

if (!validateMetric(body)) {
const parsed = vitalsSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ success: false, message: 'Invalid metric payload' },
{
success: false,
message: 'Invalid metric payload',
errors: parsed.error.flatten().fieldErrors,
},
{ status: 400 },
);
}

const { name, value, rating, url, timestamp } = body;
const { name, value, rating, url, timestamp } = parsed.data;

const result = await query(
`INSERT INTO web_vitals (name, value, rating, page_url, created_at)
Expand Down
10 changes: 9 additions & 1 deletion src/lib/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';

/**
* Validates the Authorization header and returns a 401 response if missing or invalid.
* Checks for authentication via Bearer token or internal API secret.
* Returns a 401 response if neither is valid, or null if authorized.
* Usage: const unauth = requireAuth(request); if (unauth) return unauth;
*/
export function requireAuth(request: NextRequest): NextResponse | null {
const internalToken = request.headers.get('x-internal-token');
const internalSecret = process.env.INTERNAL_API_SECRET;

if (internalToken && internalSecret && internalToken === internalSecret) {
return null;
}

const authHeader = request.headers.get('authorization');

if (!authHeader || !authHeader.startsWith('Bearer ')) {
Expand Down
Loading