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>
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user