588 lines
18 KiB
TypeScript
588 lines
18 KiB
TypeScript
import type { Database } from "@/lib/types";
|
|
import { listPleskDomains, listPleskSubscriptions } from "@/lib/plesk/client";
|
|
|
|
type SupabaseLike = any;
|
|
|
|
type SyncResult = {
|
|
instanceId: string;
|
|
agencyId: string;
|
|
skipped: boolean;
|
|
reason?: string;
|
|
subscriptionsUpserted: number;
|
|
domainsUpserted: number;
|
|
};
|
|
|
|
type InstanceRow = Pick<
|
|
Database["public"]["Tables"]["plesk_instances"]["Row"],
|
|
| "id"
|
|
| "agency_id"
|
|
| "base_url"
|
|
| "auth_type"
|
|
| "encrypted_secret"
|
|
| "last_connected_at"
|
|
>;
|
|
|
|
type LocalSubscriptionLookupRow = {
|
|
id: string;
|
|
plesk_subscription_id: string;
|
|
name: string | null;
|
|
status: string | null;
|
|
};
|
|
|
|
const MIN_INTERVAL_MS = 3 * 60 * 1000;
|
|
|
|
function normalizeDomainLike(value: string) {
|
|
return value.trim().toLowerCase().replace(/\.$/, "");
|
|
}
|
|
|
|
function findBestSuffixSubscriptionMatch<
|
|
T extends { normalizedName: string; id: string; status: string | null },
|
|
>(normalizedDomain: string, candidates: T[]) {
|
|
let best: T | undefined;
|
|
|
|
for (const candidate of candidates) {
|
|
if (!normalizedDomain.endsWith(`.${candidate.normalizedName}`)) continue;
|
|
if (!best || candidate.normalizedName.length > best.normalizedName.length) {
|
|
best = candidate;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
export async function syncPleskInstance(
|
|
supabase: SupabaseLike,
|
|
instance: InstanceRow,
|
|
actorUserId?: string | null,
|
|
action = "sync",
|
|
): Promise<SyncResult> {
|
|
const startedAtIso = new Date().toISOString();
|
|
|
|
const logStatusSyncEvent = async (
|
|
eventAction:
|
|
| "subscription_status_sync_started"
|
|
| "subscription_status_sync_completed"
|
|
| "subscription_status_sync_failed",
|
|
metadata?: Record<string, unknown>,
|
|
status: "success" | "failed" = "success",
|
|
errorMessage?: string | null,
|
|
) => {
|
|
try {
|
|
await supabase.from("actions_log").insert({
|
|
agency_id: instance.agency_id,
|
|
actor_user_id: actorUserId ?? null,
|
|
target_type: "plesk_instance",
|
|
target_id: instance.id,
|
|
action: eventAction,
|
|
status,
|
|
error_message: errorMessage ?? null,
|
|
metadata: metadata ?? {},
|
|
});
|
|
} catch {
|
|
// Logging should never block sync flow.
|
|
}
|
|
};
|
|
|
|
await supabase
|
|
.from("plesk_instances")
|
|
.update({
|
|
status: "syncing",
|
|
last_sync_error: null,
|
|
})
|
|
.eq("id", instance.id)
|
|
.eq("agency_id", instance.agency_id);
|
|
|
|
const last = instance.last_connected_at
|
|
? new Date(instance.last_connected_at).getTime()
|
|
: 0;
|
|
if (last && Date.now() - last < MIN_INTERVAL_MS) {
|
|
await supabase
|
|
.from("plesk_instances")
|
|
.update({
|
|
status: "connected",
|
|
last_sync_at: startedAtIso,
|
|
last_sync_status: "skipped",
|
|
last_sync_error: "recently_synced",
|
|
})
|
|
.eq("id", instance.id)
|
|
.eq("agency_id", instance.agency_id);
|
|
|
|
return {
|
|
instanceId: instance.id,
|
|
agencyId: instance.agency_id,
|
|
skipped: true,
|
|
reason: "recently_synced",
|
|
subscriptionsUpserted: 0,
|
|
domainsUpserted: 0,
|
|
};
|
|
}
|
|
|
|
try {
|
|
await logStatusSyncEvent("subscription_status_sync_started", {
|
|
started_at: startedAtIso,
|
|
});
|
|
|
|
const [subscriptionsRaw, domainsRaw] = await Promise.all([
|
|
listPleskSubscriptions({
|
|
baseUrl: instance.base_url,
|
|
authType: instance.auth_type,
|
|
encryptedSecret: instance.encrypted_secret,
|
|
}),
|
|
listPleskDomains({
|
|
baseUrl: instance.base_url,
|
|
authType: instance.auth_type,
|
|
encryptedSecret: instance.encrypted_secret,
|
|
}),
|
|
]);
|
|
|
|
const subscriptionsForUpsert = subscriptionsRaw
|
|
.map((item: any) => {
|
|
const sourceId =
|
|
item?.plesk_subscription_id ??
|
|
item?.name ??
|
|
item?.id ??
|
|
item?.guid ??
|
|
item?.subscription_id;
|
|
if (!sourceId) return null;
|
|
return {
|
|
agency_id: instance.agency_id,
|
|
plesk_instance_id: instance.id,
|
|
plesk_subscription_id: String(sourceId),
|
|
name: item?.name ? String(item.name) : String(sourceId),
|
|
status: item?.status ? String(item.status) : "unknown",
|
|
status_raw: item?.status_raw ? String(item.status_raw) : null,
|
|
last_status_sync_at: startedAtIso,
|
|
owner_login: item?.owner_login
|
|
? String(item.owner_login)
|
|
: item?.owner?.login
|
|
? String(item.owner.login)
|
|
: null,
|
|
plan_name: item?.plan_name
|
|
? String(item.plan_name)
|
|
: item?.service_plan?.name
|
|
? String(item.service_plan.name)
|
|
: null,
|
|
};
|
|
})
|
|
.filter(Boolean) as Array<Record<string, unknown>>;
|
|
|
|
const subscriptionStatusSyncFailures = subscriptionsRaw.filter(
|
|
(item: any) => Boolean(item?.status_sync_error),
|
|
);
|
|
|
|
if (subscriptionsForUpsert.length > 0) {
|
|
const { error } = await supabase
|
|
.from("plesk_subscriptions")
|
|
.upsert(subscriptionsForUpsert, {
|
|
onConflict: "agency_id,plesk_subscription_id",
|
|
});
|
|
if (error) throw new Error(error.message);
|
|
}
|
|
|
|
const { data: localSubscriptions, error: localSubscriptionsError } =
|
|
await supabase
|
|
.from("plesk_subscriptions")
|
|
.select("id, plesk_subscription_id, name, status")
|
|
.eq("agency_id", instance.agency_id)
|
|
.eq("plesk_instance_id", instance.id);
|
|
|
|
if (localSubscriptionsError) {
|
|
throw new Error(localSubscriptionsError.message);
|
|
}
|
|
|
|
const subscriptionsIndexed = ((localSubscriptions ?? []) as Array<any>)
|
|
.filter((row): row is LocalSubscriptionLookupRow =>
|
|
Boolean(row?.plesk_subscription_id && row?.id),
|
|
)
|
|
.map((row) => ({
|
|
id: String(row.id),
|
|
pleskSubscriptionId: String(row.plesk_subscription_id),
|
|
status: row?.status ? String(row.status) : null,
|
|
name: row?.name ? String(row.name) : null,
|
|
}));
|
|
|
|
const subscriptionByExternalId = new Map<
|
|
string,
|
|
{
|
|
id: string;
|
|
pleskSubscriptionId: string;
|
|
status: string | null;
|
|
name: string | null;
|
|
}
|
|
>(subscriptionsIndexed.map((row) => [row.pleskSubscriptionId, row]));
|
|
|
|
const subscriptionByLocalId = new Map<
|
|
string,
|
|
{
|
|
id: string;
|
|
pleskSubscriptionId: string;
|
|
status: string | null;
|
|
name: string | null;
|
|
}
|
|
>(subscriptionsIndexed.map((row) => [row.id, row]));
|
|
|
|
const subscriptionByName = new Map<
|
|
string,
|
|
{
|
|
id: string;
|
|
pleskSubscriptionId: string;
|
|
status: string | null;
|
|
name: string | null;
|
|
}
|
|
>(
|
|
subscriptionsIndexed
|
|
.filter((row) => row.name)
|
|
.map((row) => [normalizeDomainLike(String(row.name)), row]),
|
|
);
|
|
|
|
const subscriptionNameCandidates: Array<{
|
|
id: string;
|
|
pleskSubscriptionId: string;
|
|
status: string | null;
|
|
normalizedName: string;
|
|
}> = subscriptionsIndexed
|
|
.filter((row) => row.name)
|
|
.map((row) => ({
|
|
id: row.id,
|
|
pleskSubscriptionId: row.pleskSubscriptionId,
|
|
status: row.status,
|
|
normalizedName: normalizeDomainLike(String(row.name)),
|
|
}));
|
|
|
|
const domainsForUpsert = domainsRaw
|
|
.map((item: any) => {
|
|
const domainId = item?.id ?? item?.guid ?? item?.name;
|
|
const domainName = item?.name ?? item?.domain_name ?? item?.domain;
|
|
const normalizedDomainName = domainName
|
|
? normalizeDomainLike(String(domainName))
|
|
: null;
|
|
const subExternalId =
|
|
item?.subscription?.id ?? item?.subscription_id ?? item?.webspace_id;
|
|
const rawExternalSubscriptionId = subExternalId
|
|
? String(subExternalId)
|
|
: null;
|
|
const subscriptionNameHint =
|
|
item?.subscription?.name ??
|
|
item?.subscription_name ??
|
|
item?.webspace_name ??
|
|
item?.webspace ??
|
|
null;
|
|
|
|
const localSubscriptionByExternal = rawExternalSubscriptionId
|
|
? subscriptionByExternalId.get(rawExternalSubscriptionId)
|
|
: undefined;
|
|
const localSubscriptionByLocalId = rawExternalSubscriptionId
|
|
? subscriptionByLocalId.get(rawExternalSubscriptionId)
|
|
: undefined;
|
|
const localSubscriptionByName = subscriptionNameHint
|
|
? subscriptionByName.get(
|
|
normalizeDomainLike(String(subscriptionNameHint)),
|
|
)
|
|
: undefined;
|
|
const localSubscriptionByDomainName = normalizedDomainName
|
|
? subscriptionByName.get(normalizedDomainName)
|
|
: undefined;
|
|
const localSubscriptionBySuffix = normalizedDomainName
|
|
? findBestSuffixSubscriptionMatch(
|
|
normalizedDomainName,
|
|
subscriptionNameCandidates,
|
|
)
|
|
: undefined;
|
|
const localSubscription =
|
|
localSubscriptionByExternal ??
|
|
localSubscriptionByLocalId ??
|
|
localSubscriptionByName ??
|
|
localSubscriptionByDomainName ??
|
|
localSubscriptionBySuffix;
|
|
|
|
const externalSubscriptionId = localSubscription
|
|
? localSubscription.pleskSubscriptionId
|
|
: subscriptionNameHint
|
|
? String(subscriptionNameHint)
|
|
: rawExternalSubscriptionId;
|
|
const rawCreated = item?.created_at ?? item?.created;
|
|
const sourceCreatedAt = rawCreated
|
|
? new Date(String(rawCreated)).toISOString()
|
|
: null;
|
|
const aliasesCount =
|
|
typeof item?.aliases_count === "number"
|
|
? item.aliases_count
|
|
: Array.isArray(item?.aliases)
|
|
? item.aliases.length
|
|
: null;
|
|
|
|
if (!domainId || !domainName) return null;
|
|
|
|
return {
|
|
agency_id: instance.agency_id,
|
|
plesk_instance_id: instance.id,
|
|
subscription_id: localSubscription?.id ?? null,
|
|
external_subscription_id: externalSubscriptionId,
|
|
plesk_domain_id: String(domainId),
|
|
domain_name: String(domainName),
|
|
status: localSubscription?.status ?? "unknown",
|
|
hosting_type: item?.hosting_type ? String(item.hosting_type) : null,
|
|
source_created_at: sourceCreatedAt,
|
|
aliases_count: aliasesCount,
|
|
};
|
|
})
|
|
.filter(Boolean) as Array<Record<string, unknown>>;
|
|
|
|
const unlinkedDomains = domainsForUpsert.filter(
|
|
(domain) => !domain.subscription_id,
|
|
);
|
|
|
|
if (domainsForUpsert.length > 0) {
|
|
const { error } = await supabase
|
|
.from("plesk_domains")
|
|
.upsert(domainsForUpsert, {
|
|
onConflict: "agency_id,plesk_domain_id",
|
|
});
|
|
if (error) throw new Error(error.message);
|
|
}
|
|
|
|
await supabase
|
|
.from("plesk_instances")
|
|
.update({
|
|
last_connected_at: new Date().toISOString(),
|
|
status: "connected",
|
|
error_message: null,
|
|
last_sync_at: startedAtIso,
|
|
last_sync_status: "success",
|
|
last_sync_error: null,
|
|
last_sync_subscriptions: subscriptionsForUpsert.length,
|
|
last_sync_domains: domainsForUpsert.length,
|
|
})
|
|
.eq("id", instance.id)
|
|
.eq("agency_id", instance.agency_id);
|
|
|
|
await supabase.from("actions_log").insert({
|
|
agency_id: instance.agency_id,
|
|
actor_user_id: actorUserId ?? null,
|
|
target_type: "plesk_instance",
|
|
target_id: instance.id,
|
|
action,
|
|
status: "success",
|
|
metadata: {
|
|
subscriptions_seen: subscriptionsRaw.length,
|
|
domains_seen: domainsRaw.length,
|
|
unlinked_domains: unlinkedDomains.length,
|
|
unlinked_domain_samples: unlinkedDomains
|
|
.slice(0, 25)
|
|
.map((domain) => String(domain.domain_name ?? "unknown")),
|
|
},
|
|
});
|
|
|
|
await logStatusSyncEvent("subscription_status_sync_completed", {
|
|
synced_subscriptions: subscriptionsRaw.length,
|
|
failed_subscriptions: subscriptionStatusSyncFailures.length,
|
|
failed_subscription_ids: subscriptionStatusSyncFailures
|
|
.slice(0, 50)
|
|
.map((item: any) =>
|
|
String(
|
|
item?.plesk_subscription_id ?? item?.subscription_id ?? "unknown",
|
|
),
|
|
),
|
|
completed_at: new Date().toISOString(),
|
|
});
|
|
|
|
if (subscriptionStatusSyncFailures.length > 0) {
|
|
await logStatusSyncEvent(
|
|
"subscription_status_sync_failed",
|
|
{
|
|
failed_subscriptions: subscriptionStatusSyncFailures.length,
|
|
sample_errors: subscriptionStatusSyncFailures
|
|
.slice(0, 20)
|
|
.map((item: any) => ({
|
|
subscription: String(
|
|
item?.plesk_subscription_id ??
|
|
item?.subscription_id ??
|
|
"unknown",
|
|
),
|
|
error: String(item?.status_sync_error ?? "status_sync_failed"),
|
|
})),
|
|
},
|
|
"failed",
|
|
`${subscriptionStatusSyncFailures.length} subscription status sync failures`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
instanceId: instance.id,
|
|
agencyId: instance.agency_id,
|
|
skipped: false,
|
|
subscriptionsUpserted: subscriptionsForUpsert.length,
|
|
domainsUpserted: domainsForUpsert.length,
|
|
};
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message.slice(0, 1000) : "Sync failed";
|
|
|
|
await logStatusSyncEvent(
|
|
"subscription_status_sync_failed",
|
|
{
|
|
failed_at: new Date().toISOString(),
|
|
},
|
|
"failed",
|
|
message,
|
|
);
|
|
|
|
await supabase
|
|
.from("plesk_instances")
|
|
.update({
|
|
status: "error",
|
|
error_message: message,
|
|
last_sync_at: startedAtIso,
|
|
last_sync_status: "failed",
|
|
last_sync_error: message,
|
|
})
|
|
.eq("id", instance.id)
|
|
.eq("agency_id", instance.agency_id);
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function backfillDomainSubscriptionLinks(
|
|
supabase: SupabaseLike,
|
|
input: {
|
|
agencyId: string;
|
|
instanceId: string;
|
|
actorUserId?: string | null;
|
|
},
|
|
) {
|
|
const { agencyId, instanceId, actorUserId } = input;
|
|
|
|
const { data: subscriptions, error: subscriptionsError } = await supabase
|
|
.from("plesk_subscriptions")
|
|
.select("id, plesk_subscription_id, name, status")
|
|
.eq("agency_id", agencyId)
|
|
.eq("plesk_instance_id", instanceId);
|
|
|
|
if (subscriptionsError) {
|
|
throw new Error(subscriptionsError.message);
|
|
}
|
|
|
|
const { data: domains, error: domainsError } = await supabase
|
|
.from("plesk_domains")
|
|
.select("id, domain_name, external_subscription_id, subscription_id")
|
|
.eq("agency_id", agencyId)
|
|
.eq("plesk_instance_id", instanceId)
|
|
.is("subscription_id", null);
|
|
|
|
if (domainsError) {
|
|
throw new Error(domainsError.message);
|
|
}
|
|
|
|
const subscriptionByExternalId = new Map<
|
|
string,
|
|
{ id: string; status: string }
|
|
>(
|
|
(subscriptions ?? [])
|
|
.filter((subscription: any) => subscription?.plesk_subscription_id)
|
|
.map((subscription: any) => [
|
|
String(subscription.plesk_subscription_id),
|
|
{
|
|
id: String(subscription.id),
|
|
status: subscription?.status
|
|
? String(subscription.status)
|
|
: "unknown",
|
|
},
|
|
]),
|
|
);
|
|
|
|
const subscriptionByName = new Map<string, { id: string; status: string }>(
|
|
(subscriptions ?? [])
|
|
.filter((subscription: any) => subscription?.name)
|
|
.map((subscription: any) => [
|
|
normalizeDomainLike(String(subscription.name)),
|
|
{
|
|
id: String(subscription.id),
|
|
status: subscription?.status
|
|
? String(subscription.status)
|
|
: "unknown",
|
|
},
|
|
]),
|
|
);
|
|
|
|
const subscriptionNameCandidates: Array<{
|
|
id: string;
|
|
status: string;
|
|
normalizedName: string;
|
|
}> = (subscriptions ?? [])
|
|
.filter((subscription: any) => subscription?.name)
|
|
.map((subscription: any) => ({
|
|
id: String(subscription.id),
|
|
status: subscription?.status ? String(subscription.status) : "unknown",
|
|
normalizedName: normalizeDomainLike(String(subscription.name)),
|
|
}));
|
|
|
|
let linkedCount = 0;
|
|
const failures: Array<{ domain_id: string; error: string }> = [];
|
|
|
|
for (const domain of domains ?? []) {
|
|
const matchByExternal = domain?.external_subscription_id
|
|
? subscriptionByExternalId.get(String(domain.external_subscription_id))
|
|
: undefined;
|
|
const matchByDomainName = domain?.domain_name
|
|
? subscriptionByName.get(normalizeDomainLike(String(domain.domain_name)))
|
|
: undefined;
|
|
const normalizedDomainName = domain?.domain_name
|
|
? normalizeDomainLike(String(domain.domain_name))
|
|
: null;
|
|
const matchBySuffix = normalizedDomainName
|
|
? findBestSuffixSubscriptionMatch(
|
|
normalizedDomainName,
|
|
subscriptionNameCandidates,
|
|
)
|
|
: undefined;
|
|
const linkedSubscription =
|
|
matchByExternal ?? matchByDomainName ?? matchBySuffix;
|
|
|
|
if (!linkedSubscription) continue;
|
|
|
|
const { error: updateError } = await supabase
|
|
.from("plesk_domains")
|
|
.update({
|
|
subscription_id: linkedSubscription.id,
|
|
status: linkedSubscription.status,
|
|
})
|
|
.eq("id", domain.id)
|
|
.eq("agency_id", agencyId);
|
|
|
|
if (updateError) {
|
|
failures.push({
|
|
domain_id: String(domain.id),
|
|
error: updateError.message,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
linkedCount += 1;
|
|
}
|
|
|
|
await supabase.from("actions_log").insert({
|
|
agency_id: agencyId,
|
|
actor_user_id: actorUserId ?? null,
|
|
target_type: "plesk_instance",
|
|
target_id: instanceId,
|
|
action: "domain_subscription_backfill",
|
|
status: failures.length > 0 ? "failed" : "success",
|
|
error_message:
|
|
failures.length > 0
|
|
? `${failures.length} domain links failed during backfill`
|
|
: null,
|
|
metadata: {
|
|
attempted: (domains ?? []).length,
|
|
linked: linkedCount,
|
|
failures: failures.slice(0, 25),
|
|
},
|
|
});
|
|
|
|
return {
|
|
attempted: (domains ?? []).length,
|
|
linked: linkedCount,
|
|
failed: failures.length,
|
|
};
|
|
}
|