feat(proxy-pools): add support for deno deploy relays and fix layout overflow issues in proxy pools dashboard (#1437)
Co-authored-by: TD <tho.din@inno.ai.vn>
This commit is contained in:
@@ -43,7 +43,7 @@ export async function POST(request, { params }) {
|
||||
return NextResponse.json({ error: "Proxy pool not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const result = proxyPool.type === "vercel" || proxyPool.type === "cloudflare"
|
||||
const result = proxyPool.type === "vercel" || proxyPool.type === "cloudflare" || proxyPool.type === "deno"
|
||||
? await testVercelRelay(proxyPool.proxyUrl)
|
||||
: await testProxyUrl({ proxyUrl: proxyPool.proxyUrl });
|
||||
const now = new Date().toISOString();
|
||||
|
||||
175
src/app/api/proxy-pools/deno-deploy/route.js
Normal file
175
src/app/api/proxy-pools/deno-deploy/route.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createProxyPool } from "@/models";
|
||||
|
||||
const DENO_V2_API = "https://api.deno.com/v2";
|
||||
|
||||
const DENO_RELAY_CODE = `Deno.serve(async (request) => {
|
||||
const target = request.headers.get("x-relay-target");
|
||||
const relayPath = request.headers.get("x-relay-path") || "/";
|
||||
|
||||
if (!target) {
|
||||
return new Response(JSON.stringify({ error: "Missing x-relay-target header" }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const targetUrl = target.replace(/\\/$/, "") + relayPath;
|
||||
const newHeaders = new Headers(request.headers);
|
||||
newHeaders.delete("x-relay-target");
|
||||
newHeaders.delete("x-relay-path");
|
||||
newHeaders.delete("host");
|
||||
|
||||
const init = {
|
||||
method: request.method,
|
||||
headers: newHeaders,
|
||||
};
|
||||
|
||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||
init.body = request.body;
|
||||
init.duplex = "half";
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, init);
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 502,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
});`;
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const denoToken = body.denoToken?.trim();
|
||||
const orgDomain = body.orgDomain?.trim();
|
||||
const projectName = body.projectName?.trim() || `relay-${Date.now().toString(36)}`;
|
||||
|
||||
if (!orgDomain) {
|
||||
return NextResponse.json({ error: "Organization domain is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!denoToken) {
|
||||
return NextResponse.json({ error: "Deno Deploy API token is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${denoToken}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const createAppRes = await fetch(`${DENO_V2_API}/apps`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
slug: projectName,
|
||||
labels: { "custom.kind": "9router-relay" },
|
||||
config: {
|
||||
install: "deno install",
|
||||
runtime: {
|
||||
type: "dynamic",
|
||||
entrypoint: "main.ts",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createAppRes.ok) {
|
||||
const text = await createAppRes.text().catch(() => "");
|
||||
if (createAppRes.status === 409) {
|
||||
return NextResponse.json(
|
||||
{ error: `App "${projectName}" already exists. Choose a different name.` },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to create app (${createAppRes.status}): ${text}` },
|
||||
{ status: createAppRes.status }
|
||||
);
|
||||
}
|
||||
|
||||
const app = await createAppRes.json();
|
||||
|
||||
const deployRes = await fetch(`${DENO_V2_API}/apps/${app.id}/deploy`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
assets: {
|
||||
"main.ts": {
|
||||
kind: "file",
|
||||
content: DENO_RELAY_CODE,
|
||||
encoding: "utf-8",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deployRes.ok) {
|
||||
const text = await deployRes.text().catch(() => "");
|
||||
console.error("Deno Deploy error:", deployRes.status, text);
|
||||
await fetch(`${DENO_V2_API}/apps/${app.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${denoToken}` },
|
||||
}).catch(() => {});
|
||||
return NextResponse.json(
|
||||
{ error: `Deploy failed (${deployRes.status}): ${text}` },
|
||||
{ status: deployRes.status }
|
||||
);
|
||||
}
|
||||
|
||||
const revision = await deployRes.json();
|
||||
const revisionId = revision.id;
|
||||
|
||||
let status = revision.status;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30; // 30 * 2s = 60s max
|
||||
while (status === "queued" || status === "building") {
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new Error("Deploy timed out after 60 seconds");
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
const statusRes = await fetch(`${DENO_V2_API}/revisions/${revisionId}`, {
|
||||
headers: { Authorization: `Bearer ${denoToken}` },
|
||||
});
|
||||
if (!statusRes.ok) break;
|
||||
const statusData = await statusRes.json();
|
||||
status = statusData.status;
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (status !== "succeeded") {
|
||||
await fetch(`${DENO_V2_API}/apps/${app.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${denoToken}` },
|
||||
}).catch(() => {});
|
||||
return NextResponse.json(
|
||||
{ error: `Deploy failed with status: ${status}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const orgSlug = orgDomain.split(".")[0];
|
||||
const deployUrl = `https://${projectName}.${orgSlug}.deno.net`;
|
||||
console.log("Deno deployUrl:", deployUrl);
|
||||
|
||||
const proxyPool = await createProxyPool({
|
||||
name: projectName,
|
||||
proxyUrl: deployUrl,
|
||||
type: "deno",
|
||||
noProxy: "",
|
||||
isActive: true,
|
||||
strictProxy: false,
|
||||
});
|
||||
|
||||
return NextResponse.json({ proxyPool, deployUrl }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error deploying Deno Deploy relay:", error);
|
||||
return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ function toBoolean(value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const VALID_PROXY_TYPES = ["http", "vercel", "cloudflare"];
|
||||
const VALID_PROXY_TYPES = ["http", "vercel", "cloudflare", "deno"];
|
||||
|
||||
function normalizeProxyPoolInput(body = {}) {
|
||||
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||
|
||||
Reference in New Issue
Block a user