Initial working state: CL3.5 complete (Plesk sync + suspend/unsuspend)

This commit is contained in:
2026-03-05 06:47:43 +00:00
commit a36d55eae5
53 changed files with 9925 additions and 0 deletions

139
lib/agency.ts Normal file
View File

@@ -0,0 +1,139 @@
import { redirect } from "next/navigation";
import type { Database } from "@/lib/types";
import { createSupabaseAdminClient } from "@/lib/supabase/admin";
import { createSupabaseRouteClient } from "@/lib/supabase/route";
import { createSupabaseServerClient } from "@/lib/supabase/server";
export type AgencyContext = {
userId: string;
agencyId: string;
role: "owner" | "admin" | "member";
};
export function assertAgencyAdmin(context: AgencyContext) {
if (context.role !== "owner" && context.role !== "admin") {
throw new Error("Forbidden");
}
}
type AgencyMemberSummary = Pick<
Database["public"]["Tables"]["agency_members"]["Row"],
"agency_id" | "role"
>;
async function getOrCreateAgencyMembership(
userId: string,
email?: string | null,
) {
const supabase = createSupabaseServerClient() as any;
const admin = createSupabaseAdminClient() as any;
const { data: existingMembership } = (await supabase
.from("agency_members")
.select("agency_id, role")
.eq("user_id", userId)
.order("created_at", { ascending: true })
.limit(1)
.maybeSingle()) as { data: AgencyMemberSummary | null };
if (existingMembership) {
return existingMembership;
}
const defaultAgencyName = email
? `${email.split("@")[0] ?? "New"} Agency`
: "New Agency";
const { data: createdAgency, error: agencyError } = (await admin
.from("agencies")
.insert({ name: defaultAgencyName })
.select("id")
.single()) as {
data: Pick<Database["public"]["Tables"]["agencies"]["Row"], "id"> | null;
error: { message: string } | null;
};
if (agencyError || !createdAgency) {
throw new Error(
`Unable to create agency: ${agencyError?.message ?? "no row returned"}`,
);
}
const { error: membershipError } = await admin.from("agency_members").insert({
agency_id: createdAgency.id,
user_id: userId,
role: "owner",
});
if (!membershipError) {
return {
agency_id: createdAgency.id,
role: "owner" as const,
};
}
// If another request created membership first, fall back to a fresh read.
const { data: retriedMembership } = (await supabase
.from("agency_members")
.select("agency_id, role")
.eq("user_id", userId)
.order("created_at", { ascending: true })
.limit(1)
.maybeSingle()) as { data: AgencyMemberSummary | null };
if (retriedMembership) {
return retriedMembership;
}
throw new Error("Unable to create agency membership");
}
export async function getServerAgencyContextOrRedirect(): Promise<AgencyContext> {
const supabase = createSupabaseServerClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
const membership = await getOrCreateAgencyMembership(user.id, user.email);
return {
userId: user.id,
agencyId: membership.agency_id,
role: membership.role,
};
}
export async function getRouteAgencyContextOrThrow(): Promise<AgencyContext> {
const supabase = createSupabaseRouteClient();
const db = supabase as any;
const {
data: { session },
} = await supabase.auth.getSession();
if (!session?.user) {
throw new Error("Unauthorized");
}
const { data: membership } = (await db
.from("agency_members")
.select("agency_id, role")
.eq("user_id", session.user.id)
.order("created_at", { ascending: true })
.limit(1)
.maybeSingle()) as { data: AgencyMemberSummary | null };
if (!membership) {
throw new Error("No agency membership");
}
return {
userId: session.user.id,
agencyId: membership.agency_id,
role: membership.role,
};
}

18
lib/api/rate-limit.ts Normal file
View File

@@ -0,0 +1,18 @@
const buckets = new Map<string, number[]>();
export function assertRateLimit(
key: string,
maxRequests: number,
windowMs: number,
) {
const now = Date.now();
const start = now - windowMs;
const hits = (buckets.get(key) ?? []).filter((ts) => ts >= start);
if (hits.length >= maxRequests) {
throw new Error("Rate limit exceeded. Please try again in a minute.");
}
hits.push(now);
buckets.set(key, hits);
}

19
lib/api/security.ts Normal file
View File

@@ -0,0 +1,19 @@
import { env } from "@/lib/env";
export function assertSameOrigin(request: Request) {
const origin = request.headers.get("origin");
const host =
request.headers.get("x-forwarded-host") ?? request.headers.get("host");
if (!origin || !host) {
throw new Error("Invalid request origin");
}
const originUrl = new URL(origin);
const appUrl = new URL(env.appUrl);
const allowedHosts = new Set([appUrl.host, host]);
if (!allowedHosts.has(originUrl.host)) {
throw new Error("Cross-site POST blocked");
}
}

116
lib/crypto.ts Normal file
View File

@@ -0,0 +1,116 @@
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
} from "crypto";
import { env } from "@/lib/env";
const ALGO = "aes-256-gcm";
function deriveKey(secret: string) {
return createHash("sha256").update(secret).digest();
}
export function encryptSecret(value: string) {
if (!env.encryptionKey) {
throw new Error("ENCRYPTION_KEY is not configured");
}
const iv = randomBytes(12);
const key = deriveKey(env.encryptionKey);
const cipher = createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([
cipher.update(value, "utf8"),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
}
export function decryptSecret(payload: string) {
if (!env.encryptionKey) {
throw new Error("ENCRYPTION_KEY is not configured");
}
const [ivEncoded, tagEncoded, valueEncoded] = payload.split(":");
if (!ivEncoded || !tagEncoded || !valueEncoded) {
throw new Error("Invalid encrypted payload");
}
const key = deriveKey(env.encryptionKey);
const decipher = createDecipheriv(
ALGO,
key,
Buffer.from(ivEncoded, "base64"),
);
decipher.setAuthTag(Buffer.from(tagEncoded, "base64"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(valueEncoded, "base64")),
decipher.final(),
]);
return decrypted.toString("utf8");
}
function derivePleskKey() {
if (!env.pleskCredEncKey) {
throw new Error("PLESK_CRED_ENC_KEY is not configured");
}
const raw = env.pleskCredEncKey.trim();
if (/^[0-9a-fA-F]{64}$/.test(raw)) {
return Buffer.from(raw, "hex");
}
const b64 = Buffer.from(raw, "base64");
if (b64.length === 32) {
return b64;
}
throw new Error("PLESK_CRED_ENC_KEY must be 32-byte base64 or 64-char hex");
}
export function encryptPleskSecret(value: string, keyId = "v1") {
const iv = randomBytes(12);
const key = derivePleskKey();
const cipher = createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([
cipher.update(value, "utf8"),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return {
encryptedSecret: `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`,
secretKid: keyId,
};
}
export function decryptPleskSecret(payload: string) {
const [ivEncoded, tagEncoded, valueEncoded] = payload.split(":");
if (!ivEncoded || !tagEncoded || !valueEncoded) {
throw new Error("Invalid encrypted payload");
}
const key = derivePleskKey();
const decipher = createDecipheriv(
ALGO,
key,
Buffer.from(ivEncoded, "base64"),
);
decipher.setAuthTag(Buffer.from(tagEncoded, "base64"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(valueEncoded, "base64")),
decipher.final(),
]);
return decrypted.toString("utf8");
}

25
lib/env.ts Normal file
View File

@@ -0,0 +1,25 @@
const requiredEnvs = [
"NEXT_PUBLIC_SUPABASE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
] as const;
for (const key of requiredEnvs) {
if (!process.env[key]) {
// Keep as runtime warning for smoother local setup.
// eslint-disable-next-line no-console
console.warn(`[env] Missing ${key}`);
}
}
export const env = {
appUrl: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
jobSecret: process.env.JOB_SECRET ?? "",
encryptionKey: process.env.ENCRYPTION_KEY ?? "",
pleskCredEncKey: process.env.PLESK_CRED_ENC_KEY ?? "",
stripeSecretKey: process.env.STRIPE_SECRET_KEY ?? "",
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? "",
stripePriceId: process.env.STRIPE_PRICE_ID ?? "",
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "",
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
};

93
lib/plesk.ts Normal file
View File

@@ -0,0 +1,93 @@
import { decryptSecret } from "@/lib/crypto";
export type PleskAuthType = "api_key" | "basic" | "oauth_token";
export type PleskConnection = {
baseUrl: string;
authType: PleskAuthType;
authSecretEncrypted: string;
};
type PleskRequestOptions = {
method?: "GET" | "POST";
body?: unknown;
};
function buildHeaders(authType: PleskAuthType, authSecret: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
};
if (authType === "api_key") {
headers["X-API-Key"] = authSecret;
} else if (authType === "oauth_token") {
headers.Authorization = `Bearer ${authSecret}`;
} else {
headers.Authorization = `Basic ${Buffer.from(authSecret).toString("base64")}`;
}
return headers;
}
function joinUrl(baseUrl: string, path: string) {
const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return `${normalizedBase}${normalizedPath}`;
}
export async function pleskRequest<T>(
connection: PleskConnection,
path: string,
options: PleskRequestOptions = {},
) {
const authSecret = decryptSecret(connection.authSecretEncrypted);
const response = await fetch(joinUrl(connection.baseUrl, path), {
method: options.method ?? "GET",
headers: buildHeaders(connection.authType, authSecret),
body: options.body ? JSON.stringify(options.body) : undefined,
cache: "no-store",
});
if (!response.ok) {
const message = await response.text();
throw new Error(`Plesk request failed (${response.status}): ${message}`);
}
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
return null as T;
}
return (await response.json()) as T;
}
export async function validatePleskConnection(connection: PleskConnection) {
try {
await pleskRequest(connection, "/api/v2/server");
return true;
} catch {
await pleskRequest(connection, "/api/v2/subscriptions");
return true;
}
}
export async function listPleskSubscriptions(connection: PleskConnection): Promise<any[]> {
const result = await pleskRequest<any[]>(connection, "/api/v2/subscriptions");
return Array.isArray(result) ? result : [];
}
export async function listPleskDomains(connection: PleskConnection): Promise<any[]> {
const result = await pleskRequest<any[]>(connection, "/api/v2/domains");
return Array.isArray(result) ? result : [];
}
export async function suspendPleskSubscription(connection: PleskConnection, subscriptionId: string) {
return pleskRequest(connection, `/api/v2/subscriptions/${subscriptionId}/suspend`, { method: "POST" });
}
export async function unsuspendPleskSubscription(connection: PleskConnection, subscriptionId: string) {
return pleskRequest(connection, `/api/v2/subscriptions/${subscriptionId}/unsuspend`, {
method: "POST",
});
}

248
lib/plesk/client.ts Normal file
View File

@@ -0,0 +1,248 @@
import { decryptPleskSecret } from "@/lib/crypto";
export type PleskAuthType = "api_key" | "basic";
export type PleskConnection = {
baseUrl: string;
authType: PleskAuthType;
encryptedSecret: string;
};
export type PleskSubscription = {
id?: string | number;
guid?: string;
subscription_id?: string | number;
external_id?: string;
name?: string;
status?: string;
owner_login?: string;
owner?: { login?: string };
plan_name?: string;
service_plan?: { name?: string };
};
export type PleskDomain = {
id?: string | number;
guid?: string;
name?: string;
domain_name?: string;
domain?: string;
status?: string;
subscription?: { id?: string | number };
subscription_id?: string | number;
webspace_id?: string | number;
};
type PleskCliResult = {
code?: number;
stdout?: string;
stderr?: string;
};
const DEFAULT_TIMEOUT_MS = 12_000;
function normalizeBaseUrl(baseUrl: string) {
return baseUrl.trim().replace(/\/+$/, "");
}
function buildHeaders(
authType: PleskAuthType,
plainSecret: string,
): Record<string, string> {
const headers: Record<string, string> = {
Accept: "application/json",
"Content-Type": "application/json",
};
if (authType === "api_key") {
headers["X-API-Key"] = plainSecret;
} else {
headers.Authorization = `Basic ${Buffer.from(plainSecret).toString("base64")}`;
}
return headers;
}
async function withTimeout(
input: RequestInfo | URL,
init: RequestInit,
timeoutMs = DEFAULT_TIMEOUT_MS,
) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(input, {
...init,
signal: controller.signal,
cache: "no-store",
});
} finally {
clearTimeout(timeout);
}
}
export async function pleskRequest<T>(
connection: PleskConnection,
path: string,
init: RequestInit = {},
retries = 1,
) {
const base = normalizeBaseUrl(connection.baseUrl);
const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
const secret = decryptPleskSecret(connection.encryptedSecret);
let lastError: Error | null = null;
for (let attempt = 0; attempt <= retries; attempt += 1) {
try {
const res = await withTimeout(url, {
...init,
headers: {
...buildHeaders(connection.authType, secret),
...(init.headers ?? {}),
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(
`Plesk request failed (${res.status}): ${text || "unknown error"}`,
);
}
const contentType = res.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
return null as T;
}
return (await res.json()) as T;
} catch (error) {
lastError =
error instanceof Error ? error : new Error("Plesk request failed");
if (attempt === retries) break;
await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)));
}
}
throw lastError ?? new Error("Plesk request failed");
}
export async function testPleskConnection(connection: PleskConnection) {
try {
await pleskRequest(connection, "/api/v2/server");
return { ok: true as const };
} catch {
await pleskRequest(connection, "/api/v2/subscriptions?limit=1");
return { ok: true as const };
}
}
export async function listPleskSubscriptions(connection: PleskConnection) {
try {
const all: PleskSubscription[] = [];
for (let page = 1; page <= 20; page += 1) {
const payload = await pleskRequest<PleskSubscription[]>(
connection,
`/api/v2/subscriptions?page=${page}&per_page=100`,
);
const rows = Array.isArray(payload) ? payload : [];
all.push(...rows);
if (rows.length < 100) break;
}
return all;
} catch {
const cli = await pleskRequest<PleskCliResult>(
connection,
"/api/v2/cli/subscription/call",
{
method: "POST",
body: JSON.stringify({ params: ["--list"] }),
},
);
const stdout = cli?.stdout ?? "";
if (!stdout.trim()) {
return [];
}
return stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const columns = line.split(/\s+/);
const externalId = columns[0] ?? line;
return {
id: externalId,
subscription_id: externalId,
external_id: externalId,
name: columns.slice(1).join(" ") || externalId,
status: null,
};
});
}
}
export async function pleskCliCall(
connection: PleskConnection,
command: string,
params: string[],
) {
const payload = await pleskRequest<PleskCliResult>(
connection,
`/api/v2/cli/${command}/call`,
{
method: "POST",
body: JSON.stringify({ params }),
},
);
const code = payload?.code ?? -1;
const stdout = payload?.stdout ?? "";
const stderr = payload?.stderr ?? "";
if (code !== 0) {
throw new Error(
`Plesk CLI '${command}' failed (code ${code}): ${stderr || "unknown error"}${stdout ? `\n${stdout}` : ""}`,
);
}
return { code, stdout, stderr };
}
export async function listPleskDomains(connection: PleskConnection) {
const all: PleskDomain[] = [];
for (let page = 1; page <= 20; page += 1) {
const payload = await pleskRequest<PleskDomain[]>(
connection,
`/api/v2/domains?page=${page}&per_page=100`,
);
const rows = Array.isArray(payload) ? payload : [];
all.push(...rows);
if (rows.length < 100) break;
}
return all;
}
export async function suspendPleskSubscription(
connection: PleskConnection,
subscriptionName: string,
) {
return pleskCliCall(connection, "subscription", [
"--webspace-off",
subscriptionName,
]);
}
export async function unsuspendPleskSubscription(
connection: PleskConnection,
subscriptionName: string,
) {
return pleskCliCall(connection, "subscription", [
"--webspace-on",
subscriptionName,
]);
}

218
lib/plesk/sync.ts Normal file
View File

@@ -0,0 +1,218 @@
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"
>;
const MIN_INTERVAL_MS = 3 * 60 * 1000;
export async function syncPleskInstance(
supabase: SupabaseLike,
instance: InstanceRow,
actorUserId?: string | null,
action = "sync",
): Promise<SyncResult> {
const startedAtIso = new Date().toISOString();
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 {
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?.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) : null,
status: item?.status ? String(item.status) : null,
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>>;
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 domainsForUpsert = domainsRaw
.map((item: any) => {
const domainId = item?.id ?? item?.guid ?? item?.name;
const domainName = item?.name ?? item?.domain_name ?? item?.domain;
const externalSubscriptionId =
item?.subscription?.id ?? item?.subscription_id ?? item?.webspace_id;
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: null,
external_subscription_id: externalSubscriptionId
? String(externalSubscriptionId)
: null,
plesk_domain_id: String(domainId),
domain_name: String(domainName),
status: item?.status ? String(item.status) : null,
hosting_type: item?.hosting_type
? String(item.hosting_type)
: item?.hosting?.type
? String(item.hosting.type)
: null,
source_created_at: sourceCreatedAt,
aliases_count: aliasesCount,
};
})
.filter(Boolean) as Array<Record<string, unknown>>;
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,
},
});
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 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;
}
}

19
lib/stripe.ts Normal file
View File

@@ -0,0 +1,19 @@
import Stripe from "stripe";
import { env } from "@/lib/env";
let stripeClient: Stripe | null = null;
export function getStripeClient() {
if (!env.stripeSecretKey) {
throw new Error("STRIPE_SECRET_KEY is not configured");
}
if (!stripeClient) {
stripeClient = new Stripe(env.stripeSecretKey, {
apiVersion: "2025-02-24.acacia",
});
}
return stripeClient;
}

13
lib/supabase/admin.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createClient } from "@supabase/supabase-js";
import { env } from "@/lib/env";
import type { Database } from "@/lib/types";
export function createSupabaseAdminClient() {
return createClient<Database>(env.supabaseUrl, env.supabaseServiceRoleKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
}

9
lib/supabase/browser.ts Normal file
View File

@@ -0,0 +1,9 @@
"use client";
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
import type { Database } from "@/lib/types";
export function createSupabaseBrowserClient() {
return createClientComponentClient<Database>();
}

8
lib/supabase/route.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import type { Database } from "@/lib/types";
export function createSupabaseRouteClient() {
return createRouteHandlerClient<Database>({ cookies });
}

8
lib/supabase/server.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import type { Database } from "@/lib/types";
export function createSupabaseServerClient() {
return createServerComponentClient<Database>({ cookies });
}

311
lib/types.ts Normal file
View File

@@ -0,0 +1,311 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json }
| Json[];
export type Database = {
public: {
Tables: {
agencies: {
Row: { id: string; name: string; created_at: string };
Insert: { id?: string; name: string; created_at?: string };
Update: { id?: string; name?: string; created_at?: string };
};
agency_members: {
Row: {
id: string;
agency_id: string;
user_id: string;
role: "owner" | "admin" | "member";
created_at: string;
};
Insert: {
id?: string;
agency_id: string;
user_id: string;
role: "owner" | "admin" | "member";
created_at?: string;
};
Update: {
id?: string;
agency_id?: string;
user_id?: string;
role?: "owner" | "admin" | "member";
created_at?: string;
};
};
clients: {
Row: {
id: string;
agency_id: string;
name: string;
email: string | null;
phone: string | null;
notes: string | null;
created_at: string;
};
Insert: {
id?: string;
agency_id: string;
name: string;
email?: string | null;
phone?: string | null;
notes?: string | null;
created_at?: string;
};
Update: {
id?: string;
agency_id?: string;
name?: string;
email?: string | null;
phone?: string | null;
notes?: string | null;
created_at?: string;
};
};
plesk_instances: {
Row: {
id: string;
agency_id: string;
name: string;
base_url: string;
auth_type: "api_key" | "basic";
encrypted_secret: string;
secret_kid: string | null;
status: "connected" | "error" | "disabled" | "syncing";
error_message: string | null;
last_connected_at: string | null;
last_sync_at: string | null;
last_sync_status: string | null;
last_sync_error: string | null;
last_sync_subscriptions: number;
last_sync_domains: number;
created_at: string;
};
Insert: {
id?: string;
agency_id: string;
name: string;
base_url: string;
auth_type: "api_key" | "basic";
encrypted_secret: string;
secret_kid?: string | null;
status?: "connected" | "error" | "disabled" | "syncing";
error_message?: string | null;
last_connected_at?: string | null;
last_sync_at?: string | null;
last_sync_status?: string | null;
last_sync_error?: string | null;
last_sync_subscriptions?: number;
last_sync_domains?: number;
created_at?: string;
};
Update: {
id?: string;
agency_id?: string;
name?: string;
base_url?: string;
auth_type?: "api_key" | "basic";
encrypted_secret?: string;
secret_kid?: string | null;
status?: "connected" | "error" | "disabled" | "syncing";
error_message?: string | null;
last_connected_at?: string | null;
last_sync_at?: string | null;
last_sync_status?: string | null;
last_sync_error?: string | null;
last_sync_subscriptions?: number;
last_sync_domains?: number;
created_at?: string;
};
};
plesk_subscriptions: {
Row: {
id: string;
agency_id: string;
plesk_instance_id: string;
client_id: string | null;
plesk_subscription_id: string;
name: string | null;
status: string | null;
owner_login: string | null;
plan_name: string | null;
created_at: string;
updated_at: string;
};
Insert: {
id?: string;
agency_id: string;
plesk_instance_id: string;
client_id?: string | null;
plesk_subscription_id: string;
name?: string | null;
status?: string | null;
owner_login?: string | null;
plan_name?: string | null;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
agency_id?: string;
plesk_instance_id?: string;
client_id?: string | null;
plesk_subscription_id?: string;
name?: string | null;
status?: string | null;
owner_login?: string | null;
plan_name?: string | null;
created_at?: string;
updated_at?: string;
};
};
plesk_domains: {
Row: {
id: string;
agency_id: string;
plesk_instance_id: string;
subscription_id: string | null;
external_subscription_id: string | null;
plesk_domain_id: string;
domain_name: string;
status: string | null;
hosting_type: string | null;
source_created_at: string | null;
aliases_count: number | null;
updated_at: string;
created_at: string;
};
Insert: {
id?: string;
agency_id: string;
plesk_instance_id: string;
subscription_id?: string | null;
external_subscription_id?: string | null;
plesk_domain_id: string;
domain_name: string;
status?: string | null;
hosting_type?: string | null;
source_created_at?: string | null;
aliases_count?: number | null;
updated_at?: string;
created_at?: string;
};
Update: {
id?: string;
agency_id?: string;
plesk_instance_id?: string;
subscription_id?: string | null;
external_subscription_id?: string | null;
plesk_domain_id?: string;
domain_name?: string;
status?: string | null;
hosting_type?: string | null;
source_created_at?: string | null;
aliases_count?: number | null;
updated_at?: string;
created_at?: string;
};
};
actions_log: {
Row: {
id: string;
agency_id: string;
actor_user_id: string | null;
target_type: string;
target_id: string | null;
action: string;
status: "pending" | "success" | "failed";
error_message: string | null;
metadata: Json;
created_at: string;
};
Insert: {
id?: string;
agency_id: string;
actor_user_id?: string | null;
target_type: string;
target_id?: string | null;
action: string;
status?: "pending" | "success" | "failed";
error_message?: string | null;
metadata?: Json;
created_at?: string;
};
Update: {
id?: string;
agency_id?: string;
actor_user_id?: string | null;
target_type?: string;
target_id?: string | null;
action?: string;
status?: "pending" | "success" | "failed";
error_message?: string | null;
metadata?: Json;
created_at?: string;
};
};
billing_accounts: {
Row: {
id: string;
agency_id: string;
stripe_customer_id: string | null;
stripe_subscription_id: string | null;
plan_id: string | null;
subscription_status: string | null;
current_period_end: string | null;
created_at: string;
updated_at: string;
};
Insert: {
id?: string;
agency_id: string;
stripe_customer_id?: string | null;
stripe_subscription_id?: string | null;
plan_id?: string | null;
subscription_status?: string | null;
current_period_end?: string | null;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
agency_id?: string;
stripe_customer_id?: string | null;
stripe_subscription_id?: string | null;
plan_id?: string | null;
subscription_status?: string | null;
current_period_end?: string | null;
created_at?: string;
updated_at?: string;
};
};
entitlements: {
Row: {
id: string;
agency_id: string;
key: string;
value: Json;
created_at: string;
};
Insert: {
id?: string;
agency_id: string;
key: string;
value?: Json;
created_at?: string;
};
Update: {
id?: string;
agency_id?: string;
key?: string;
value?: Json;
created_at?: string;
};
};
};
};
};