Files
cloudlense/website-monitoring-frontend/src/app/api/webhooks/website-change/route.ts
T
Dennis 14a32bdc0d feat: initialize monorepo with full dev team best practices
- Unified monorepo with backend (Express), frontend (Next.js), and devops
- Backend: ESLint, Prettier, Jest tests (3 passing), health endpoint, .env.example
- Frontend: Fixed build errors, fixed all lint errors (0 remaining), tests passing
- DevOps: Docker Compose with PostgreSQL, backend, frontend + healthchecks
- CI/CD: 3 GitHub Actions workflows (backend, frontend, docker integration)
- DX: Husky pre-commit hooks with smart change detection
- Docs: Root README with architecture, CONTRIBUTING.md, PR template

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 00:05:50 +01:00

242 lines
6.5 KiB
TypeScript

import { NextResponse } from "next/server";
import { lighthouseScanner } from "@/services/lighthouseScanner";
import { logError } from "@/utils/errorUtils";
export async function POST(request: Request) {
try {
const body = await request.json();
const { websiteId, url, changeType, contentHash, metadata } = body;
if (!websiteId || !url) {
return NextResponse.json(
{ error: "Website ID and URL are required" },
{ status: 400 }
);
}
// Structured log for production visibility
console.info(JSON.stringify({
level: 'info',
event: 'webhook_website_change_received',
websiteId,
changeType: changeType || 'unknown',
timestamp: new Date().toISOString(),
}));
// Verify the webhook signature if needed
const signature = request.headers.get('x-webhook-signature');
if (process.env.WEBHOOK_SECRET && signature) {
// Add webhook signature verification here if needed
// const isValid = verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET);
// if (!isValid) {
// return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
// }
}
// Get website details
const { getSupabaseAdmin } = await import("@/lib/admin");
const supabase = getSupabaseAdmin();
const { data: website, error: websiteError } = await supabase
.from('websites')
.select(`
id,
organization_id,
organizations!inner (
subscription_tier
)
`)
.eq('id', websiteId)
.single();
if (websiteError || !website) {
return NextResponse.json(
{ error: "Website not found" },
{ status: 404 }
);
}
// Check subscription limits
const { canScan, limits } = await lighthouseScanner.checkSubscriptionLimits(
String(website.organization_id)
);
if (!canScan) {
console.warn(JSON.stringify({
level: 'warn',
event: 'subscription_limit_exceeded',
websiteId,
timestamp: new Date().toISOString(),
}));
return NextResponse.json(
{
success: false,
message: "Subscription limit exceeded - scan skipped",
limits,
},
{ status: 429 }
);
}
// Check if change detection is enabled for this subscription
const subscriptionLimits = getSubscriptionLimits(String((website as any).organizations?.subscription_tier || 'free'));
if (!subscriptionLimits.changeDetectionEnabled) {
console.warn(JSON.stringify({
level: 'warn',
event: 'change_detection_disabled',
tier: String((website as any).organizations?.subscription_tier || 'unknown'),
websiteId,
timestamp: new Date().toISOString(),
}));
return NextResponse.json(
{
success: false,
message: "Change detection not available for this subscription tier",
},
{ status: 403 }
);
}
// Get or create the page record
let pageId: string;
const { data: existingPage } = await supabase
.from('pages')
.select('id, content_hash')
.eq('website_id', websiteId)
.eq('url', url)
.single();
if (existingPage) {
pageId = String((existingPage as any).id);
// Update the page with new content hash
await supabase
.from('pages')
.update({
content_hash: contentHash,
last_seen_at: new Date().toISOString(),
metadata: metadata || {},
})
.eq('id', pageId);
} else {
// Create new page record
const { data: newPage, error: createError } = await supabase
.from('pages')
.insert({
website_id: websiteId,
url,
path: new URL(url).pathname,
content_hash: contentHash,
title: metadata?.title || 'Unknown Page',
is_active: true,
metadata: metadata || {},
})
.select('id')
.single();
if (createError || !newPage) {
throw new Error(`Failed to create page record: ${createError?.message}`);
}
pageId = String((newPage as any).id);
}
// Trigger a high-priority scan due to changes
const scanConfig = {
websiteId,
pageId,
deviceType: 'desktop' as const,
categories: ['performance', 'accessibility', 'seo', 'best_practices'] as (
'performance' | 'accessibility' | 'seo' | 'best_practices'
)[],
priority: 'high' as const,
triggeredBy: 'change_detection' as const,
};
const result = await lighthouseScanner.performScan(scanConfig);
// Log the change detection
await supabase
.from('audit_logs')
.insert({
website_id: websiteId,
action: 'change_detected',
entity_type: 'page',
entity_id: pageId,
changes: {
change_type: changeType || 'content_update',
url,
content_hash: contentHash,
metadata,
},
});
if (result.success) {
console.info(JSON.stringify({
level: 'info',
event: 'change_detection_scan_completed',
websiteId,
timestamp: new Date().toISOString(),
}));
return NextResponse.json({
success: true,
scanId: result.scanId,
message: "Change detection scan triggered successfully",
metrics: result.metrics,
});
} else {
console.error(JSON.stringify({
level: 'error',
event: 'change_detection_scan_failed',
websiteId,
timestamp: new Date().toISOString(),
}));
return NextResponse.json(
{
success: false,
error: result.error,
},
{ status: 500 }
);
}
} catch (error) {
const errorMsg = `Error processing website change webhook: ${error instanceof Error ? error.message : 'Unknown error'}`;
logError(errorMsg, error);
return NextResponse.json(
{
success: false,
error: errorMsg,
},
{ status: 500 }
);
}
}
/**
* Get subscription limits based on tier
*/
function getSubscriptionLimits(tier: string) {
switch (tier) {
case 'free':
return {
changeDetectionEnabled: false,
};
case 'starter':
return {
changeDetectionEnabled: true,
};
case 'professional':
return {
changeDetectionEnabled: true,
};
case 'enterprise':
return {
changeDetectionEnabled: true,
};
default:
return {
changeDetectionEnabled: false,
};
}
}