From 067e0d908030ee9196b7109f485f48f9e8556899 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 5 Mar 2026 10:38:05 +0000 Subject: [PATCH] CL3.8 domain actions: suspend/unsuspend subscription from domains table --- app/api/plesk/domains/[id]/action/route.ts | 147 ++++++++++++++++++++ app/dashboard/page.tsx | 4 +- components/dashboard/dashboard-controls.tsx | 72 +++++++++- 3 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 app/api/plesk/domains/[id]/action/route.ts diff --git a/app/api/plesk/domains/[id]/action/route.ts b/app/api/plesk/domains/[id]/action/route.ts new file mode 100644 index 0000000..716b474 --- /dev/null +++ b/app/api/plesk/domains/[id]/action/route.ts @@ -0,0 +1,147 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { assertAgencyAdmin, getRouteAgencyContextOrThrow } from "@/lib/agency"; +import { assertRateLimit } from "@/lib/api/rate-limit"; +import { assertSameOrigin } from "@/lib/api/security"; +import { + suspendPleskSubscription, + unsuspendPleskSubscription, +} from "@/lib/plesk/client"; +import { createSupabaseRouteClient } from "@/lib/supabase/route"; + +const actionSchema = z.object({ + action: z.enum(["suspend", "unsuspend"]), +}); + +export async function POST( + req: Request, + { params }: { params: { id: string } }, +) { + const supabase = createSupabaseRouteClient() as any; + let context: Awaited> | null = + null; + let domain: { id: string; subscription_id: string | null } | null = null; + let subscription: { id: string; plesk_subscription_id: string } | null = null; + let requestedAction: "suspend" | "unsuspend" | null = null; + + try { + assertSameOrigin(req); + + context = await getRouteAgencyContextOrThrow(); + assertAgencyAdmin(context); + assertRateLimit(`plesk-domain-action:${context.userId}`, 10, 60_000); + + const { action } = actionSchema.parse(await req.json()); + requestedAction = action; + + const { data: domainRow, error: domainError } = await supabase + .from("plesk_domains") + .select("id, agency_id, plesk_instance_id, subscription_id") + .eq("id", params.id) + .eq("agency_id", context.agencyId) + .single(); + + if (domainError || !domainRow) { + throw new Error("Domain not found"); + } + + domain = { + id: String(domainRow.id), + subscription_id: domainRow.subscription_id + ? String(domainRow.subscription_id) + : null, + }; + + if (!domain.subscription_id) { + throw new Error("Domain is not linked to a subscription"); + } + + const { data: subscriptionRow, error: subscriptionError } = await supabase + .from("plesk_subscriptions") + .select("id, plesk_subscription_id, plesk_instance_id") + .eq("id", domain.subscription_id) + .eq("agency_id", context.agencyId) + .single(); + + if (subscriptionError || !subscriptionRow) { + throw new Error("Subscription not found"); + } + + subscription = { + id: String(subscriptionRow.id), + plesk_subscription_id: String(subscriptionRow.plesk_subscription_id), + }; + + const { data: instance, error: instanceError } = await supabase + .from("plesk_instances") + .select("id, base_url, auth_type, encrypted_secret") + .eq("id", subscriptionRow.plesk_instance_id) + .eq("agency_id", context.agencyId) + .single(); + + if (instanceError || !instance) { + throw new Error("Plesk instance not found"); + } + + const connection = { + baseUrl: instance.base_url, + authType: instance.auth_type, + encryptedSecret: instance.encrypted_secret, + } as const; + + if (action === "suspend") { + await suspendPleskSubscription( + connection, + subscription.plesk_subscription_id, + ); + } else { + await unsuspendPleskSubscription( + connection, + subscription.plesk_subscription_id, + ); + } + + await supabase.from("actions_log").insert({ + agency_id: context.agencyId, + actor_user_id: context.userId, + target_type: "plesk_domain", + target_id: domain.id, + action: `domain_${action}`, + status: "success", + metadata: { + domain_id: domain.id, + subscription_id: subscription.id, + plesk_subscription_id: subscription.plesk_subscription_id, + }, + }); + + return NextResponse.json({ ok: true }); + } catch (error) { + if (context && domain) { + const message = + error instanceof Error ? error.message : "Domain action failed"; + await supabase.from("actions_log").insert({ + agency_id: context.agencyId, + actor_user_id: context.userId, + target_type: "plesk_domain", + target_id: domain.id, + action: requestedAction ? `domain_${requestedAction}` : "domain_action", + status: "failed", + error_message: message, + metadata: { + domain_id: domain.id, + subscription_id: subscription?.id ?? domain.subscription_id, + plesk_subscription_id: subscription?.plesk_subscription_id ?? null, + }, + }); + } + + return NextResponse.json( + { + error: error instanceof Error ? error.message : "Domain action failed", + }, + { status: 400 }, + ); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1fb62b8..8ce180c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -3,7 +3,6 @@ import { DashboardControls } from "@/components/dashboard/dashboard-controls"; import { getServerAgencyContextOrRedirect } from "@/lib/agency"; import type { Database } from "@/lib/types"; import { createSupabaseServerClient } from "@/lib/supabase/server"; -import type { PostgrestError } from "@supabase/supabase-js"; type AgencySummary = Pick< Database["public"]["Tables"]["agencies"]["Row"], @@ -51,7 +50,7 @@ export default async function DashboardPage() { supabase .from("plesk_domains") .select( - "id, plesk_instance_id, domain_name, status, hosting_type, source_created_at, aliases_count, updated_at", + "id, plesk_instance_id, subscription_id, domain_name, status, hosting_type, source_created_at, aliases_count, updated_at", ) .eq("agency_id", context.agencyId) .order("updated_at", { ascending: false }) @@ -101,6 +100,7 @@ export default async function DashboardPage() { data: Array<{ id: string; plesk_instance_id: string; + subscription_id: string | null; domain_name: string; status: string | null; hosting_type: string | null; diff --git a/components/dashboard/dashboard-controls.tsx b/components/dashboard/dashboard-controls.tsx index 9af6dbd..53c0f13 100644 --- a/components/dashboard/dashboard-controls.tsx +++ b/components/dashboard/dashboard-controls.tsx @@ -31,6 +31,7 @@ type Subscription = { type Domain = { id: string; plesk_instance_id: string; + subscription_id: string | null; domain_name: string; status: string | null; hosting_type: string | null; @@ -60,9 +61,27 @@ function getDomainStatusBadge(status: string | null) { return "bg-emerald-100 text-emerald-700"; } + if (normalized.includes("ok")) { + return "bg-emerald-100 text-emerald-700"; + } + return "bg-slate-100 text-slate-600"; } +function isDomainSuspended(status: string | null) { + const normalized = (status ?? "unknown").toLowerCase(); + return normalized.includes("suspend") || normalized.includes("disabled"); +} + +function isDomainActive(status: string | null) { + const normalized = (status ?? "unknown").toLowerCase(); + return ( + normalized.includes("active") || + normalized.includes("ok") || + normalized.includes("enabled") + ); +} + export function DashboardControls({ instances, subscriptions, @@ -209,6 +228,25 @@ export function DashboardControls({ } } + async function onDomainAction( + domainId: string, + action: "suspend" | "unsuspend", + ) { + setLoading(true); + setMessage(null); + try { + await runRequest(`/api/plesk/domains/${domainId}/action`, { action }); + setMessage("Domain action completed."); + window.location.reload(); + } catch (error) { + setMessage( + error instanceof Error ? error.message : "Domain action failed", + ); + } finally { + setLoading(false); + } + } + async function onCheckout() { setLoading(true); try { @@ -537,20 +575,26 @@ export function DashboardControls({ Created Aliases Last seen + Actions {filteredDomains.length === 0 ? ( - + No domains found. ) : ( filteredDomains.map((domain) => ( - - {domain.domain_name} + +

{domain.domain_name}

+ {domain.subscription_id ? ( +

+ Subscription: {domain.subscription_id} +

+ ) : null} {formatDateTime(domain.updated_at)} + +
+ + +
+ )) )}