diff --git a/app/api/cron/health/route.ts b/app/api/cron/health/route.ts new file mode 100644 index 0000000..56a2c1d --- /dev/null +++ b/app/api/cron/health/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; + +import { env } from "@/lib/env"; +import { runHealthCheckTick } from "@/lib/plesk/health"; +import { createSupabaseAdminClient } from "@/lib/supabase/admin"; + +export async function POST(req: Request) { + const startedAt = Date.now(); + + try { + const secret = req.headers.get("x-cron-secret"); + if (!env.cronSecret || !secret || secret !== env.cronSecret) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const supabase = createSupabaseAdminClient() as any; + const summary = await runHealthCheckTick(supabase); + + return NextResponse.json({ + ...summary, + duration_ms: Date.now() - startedAt, + }); + } catch (error) { + console.error("[health cron] failed", error); + + return NextResponse.json( + { + error: "Health check job failed", + checked: 0, + ok: 0, + degraded: 0, + failed: 0, + duration_ms: Date.now() - startedAt, + }, + { status: 500 }, + ); + } +} diff --git a/app/api/plesk/instances/[id]/health/route.ts b/app/api/plesk/instances/[id]/health/route.ts new file mode 100644 index 0000000..7c7ac4d --- /dev/null +++ b/app/api/plesk/instances/[id]/health/route.ts @@ -0,0 +1,73 @@ +import { NextResponse } from "next/server"; + +import { assertAgencyAdmin, getRouteAgencyContextOrThrow } from "@/lib/agency"; +import { assertRateLimit } from "@/lib/api/rate-limit"; +import { assertSameOrigin } from "@/lib/api/security"; +import { runInstanceHealthCheck } from "@/lib/plesk/health"; +import { createSupabaseRouteClient } from "@/lib/supabase/route"; + +export async function POST( + req: Request, + { params }: { params: { id: string } }, +) { + try { + assertSameOrigin(req); + + const context = await getRouteAgencyContextOrThrow(); + assertAgencyAdmin(context); + assertRateLimit(`plesk-health-check:${context.userId}`, 20, 60_000); + + const supabase = createSupabaseRouteClient() as any; + + const { data: instance, error: instanceError } = await supabase + .from("plesk_instances") + .select( + "id, agency_id, base_url, auth_type, encrypted_secret, health_enabled, health_check_frequency_minutes, health_next_check_at, health_lock_until", + ) + .eq("id", params.id) + .eq("agency_id", context.agencyId) + .single(); + + if (instanceError || !instance) { + throw new Error("Plesk instance not found"); + } + + const result = await runInstanceHealthCheck(supabase, instance, { + actorUserId: context.userId, + force: true, + source: "manual", + }); + + if (!result.checked) { + return NextResponse.json( + { error: "Health check is already running for this instance" }, + { status: 409 }, + ); + } + + const { data: updated } = await supabase + .from("plesk_instances") + .select( + "id, health_enabled, health_status, health_last_checked_at, health_last_ok_at, health_last_error, health_last_latency_ms, health_check_frequency_minutes, health_next_check_at", + ) + .eq("id", params.id) + .eq("agency_id", context.agencyId) + .single(); + + return NextResponse.json({ + ok: true, + result, + instance: updated, + }); + } catch (error) { + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Health check execution failed", + }, + { status: 400 }, + ); + } +}