50e25e3ee8
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>
97 lines
3.6 KiB
TypeScript
97 lines
3.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|