refactor: flatten monorepo structure to backend/ frontend/ devops/

Rename subdirectories for a cleaner single-repo layout:
- website-monitoring-backend/  → backend/
- website-monitoring-frontend/ → frontend/
- website-monitoring-devops/   → devops/

Update all references in package.json scripts, CI workflows,
docker-compose, pre-commit hooks, and documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dennis
2026-03-07 00:25:29 +01:00
parent 4607af8def
commit 50e25e3ee8
253 changed files with 54 additions and 51 deletions
@@ -0,0 +1,96 @@
import { NextResponse } from "next/server";
import { getSupabaseAdmin } from "@/lib/admin";
import { TIER_LIMITS } from "@/services/tierLimits";
import { requireOrgMembership } from "@/lib/apiAuth";
/**
* GET /api/billing/usage
*
* Returns current usage vs tier limits for an organization.
* Requires authenticated user who is a member of the organization.
* Query params: ?organizationId=xxx
*/
export async function GET(request: Request) {
try {
const url = new URL(request.url);
const organizationId = url.searchParams.get("organizationId");
if (!organizationId) {
return NextResponse.json({ error: "organizationId required" }, { status: 400 });
}
// Verify caller belongs to this organization
const auth = await requireOrgMembership(organizationId, request);
if (auth instanceof NextResponse) return auth;
const supabase = getSupabaseAdmin();
// Get organization with tier info
const { data: org, error: orgError } = await supabase
.from("organizations")
.select("id, name, subscription_tier, subscription_status, created_at")
.eq("id", organizationId)
.single();
if (orgError || !org) {
return NextResponse.json({ error: "Organization not found" }, { status: 404 });
}
const tier = String(org.subscription_tier || "free");
const limits = TIER_LIMITS[tier] || TIER_LIMITS.free;
// Get current usage (parallel queries)
const startOfMonth = new Date();
startOfMonth.setDate(1);
startOfMonth.setHours(0, 0, 0, 0);
const [
{ count: websiteCount },
{ count: memberCount },
{ count: scanCountThisMonth },
{ count: scanCountTotal },
{ count: alertCount },
] = await Promise.all([
supabase.from("websites").select("*", { count: "exact", head: true }).eq("organization_id", organizationId),
supabase.from("organization_members").select("*", { count: "exact", head: true }).eq("organization_id", organizationId),
supabase
.from("scans")
.select("*", { count: "exact", head: true })
.gte("created_at", startOfMonth.toISOString()),
supabase.from("scans").select("*", { count: "exact", head: true }),
supabase.from("alerts").select("*", { count: "exact", head: true }).eq("status", "active"),
]);
const usage = {
websites: { used: websiteCount || 0, limit: limits.websites, percentage: limits.websites === -1 ? 0 : Math.round(((websiteCount || 0) / limits.websites) * 100) },
scansThisMonth: { used: scanCountThisMonth || 0, limit: limits.scansPerMonth, percentage: limits.scansPerMonth === -1 ? 0 : Math.round(((scanCountThisMonth || 0) / limits.scansPerMonth) * 100) },
teamMembers: { used: memberCount || 0, limit: limits.teamMembers, percentage: limits.teamMembers === -1 ? 0 : Math.round(((memberCount || 0) / limits.teamMembers) * 100) },
totalScans: scanCountTotal || 0,
activeAlerts: alertCount || 0,
};
return NextResponse.json({
organization: {
id: org.id,
name: org.name,
tier,
status: org.subscription_status || "active",
createdAt: org.created_at,
},
plan: limits,
usage,
features: {
scheduledScans: limits.scheduledScans,
alertNotifications: limits.alertNotifications,
competitorAnalysis: limits.competitorAnalysis,
apiAccess: limits.apiAccess,
prioritySupport: limits.prioritySupport,
},
});
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 500 }
);
}
}