14a32bdc0d
- 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>
242 lines
6.5 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|