From 81d6e1a6338d768fe7191a59c9750fece253e871 Mon Sep 17 00:00:00 2001 From: Jules Date: Wed, 22 Jul 2026 07:52:06 +0000 Subject: [PATCH] feat: unified API gateway proxy for secure docker orchestration Added a server-side API proxy gateway at /api/admin. Updated dashboard admin page to route requests via the secure proxy. Added a gateway secret mechanism to Nginx/backend to only accept authenticated proxied requests. Protected /admin and /dashboard routes via next-auth token checking in proxy.ts. Removed direct client side exposure of port 4003. --- my-portfolio/.env | 1 + my-portfolio/app/admin/page.tsx | 4 +- .../app/api/admin/[[...path]]/route.ts | 67 +++++++++++++++++++ my-portfolio/next.config.ts | 4 -- my-portfolio/proxy.ts | 12 +++- my-portfolio/server/admin-service/index.js | 9 +++ 6 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 my-portfolio/app/api/admin/[[...path]]/route.ts diff --git a/my-portfolio/.env b/my-portfolio/.env index d9cec4c..db030ad 100644 --- a/my-portfolio/.env +++ b/my-portfolio/.env @@ -1,3 +1,4 @@ NEXT_PUBLIC_STORAGE_API_URL=/api/storage NEXT_PUBLIC_MEDIA_API_URL=/api/media NEXT_PUBLIC_ADMIN_API_URL=/api/admin +NEXTAUTH_SECRET="default_dev_secret" diff --git a/my-portfolio/app/admin/page.tsx b/my-portfolio/app/admin/page.tsx index 6076dd1..139468a 100644 --- a/my-portfolio/app/admin/page.tsx +++ b/my-portfolio/app/admin/page.tsx @@ -50,7 +50,7 @@ export default function AdminPage() { const fetchContainers = async () => { try { setLoading(true); - const baseUrl = process.env.NEXT_PUBLIC_ADMIN_API_URL || (typeof window !== "undefined" ? `http://${window.location.hostname}:4003` : "http://localhost:4003"); + const baseUrl = process.env.NEXT_PUBLIC_ADMIN_API_URL || "/api/admin"; const res = await fetch(`${baseUrl}/containers`); const data = await res.json(); const mappedContainers = data.map((c: any) => ({ @@ -80,7 +80,7 @@ export default function AdminPage() { const action = currentStatus === "running" ? "stop" : "start"; try { addToast(`Requested container ${action}...`, "info"); - const baseUrl = process.env.NEXT_PUBLIC_ADMIN_API_URL || (typeof window !== "undefined" ? `http://${window.location.hostname}:4003` : "http://localhost:4003"); + const baseUrl = process.env.NEXT_PUBLIC_ADMIN_API_URL || "/api/admin"; const res = await fetch(`${baseUrl}/containers/${id}/${action}`, { method: "POST" }); if (res.ok) { addToast(`Container ${action}ed successfully`, "success"); diff --git a/my-portfolio/app/api/admin/[[...path]]/route.ts b/my-portfolio/app/api/admin/[[...path]]/route.ts new file mode 100644 index 0000000..c7c26fe --- /dev/null +++ b/my-portfolio/app/api/admin/[[...path]]/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getToken } from "next-auth/jwt"; + +const ADMIN_SERVICE_URL = process.env.ADMIN_SERVICE_URL || "http://localhost:4003"; +const GATEWAY_SECRET = process.env.GATEWAY_SECRET || "default_safe_secret_key_12345!"; + +async function proxyRequest(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); + + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const resolvedParams = await params; + const path = resolvedParams.path ? resolvedParams.path.join("/") : ""; + const searchParams = req.nextUrl.searchParams.toString(); + const queryString = searchParams ? `?${searchParams}` : ""; + const targetUrl = `${ADMIN_SERVICE_URL}/${path}${queryString}`; + + const headers = new Headers(); + req.headers.forEach((value, key) => { + // We shouldn't forward host header so fetch can set it + if (key.toLowerCase() !== "host") { + headers.set(key, value); + } + }); + + // Append the gateway secret + headers.set("x-gateway-secret", GATEWAY_SECRET); + + const init: RequestInit = { + method: req.method, + headers, + redirect: "manual", + }; + + // Only append body for methods that allow it + if (req.method !== "GET" && req.method !== "HEAD") { + const body = await req.text(); + if (body) { + init.body = body; + } + } + + try { + const response = await fetch(targetUrl, init); + const responseBody = await response.text(); + + const responseHeaders = new Headers(response.headers); + responseHeaders.delete("content-encoding"); + + return new NextResponse(responseBody, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error) { + console.error("Proxy error:", error); + return NextResponse.json({ error: "Gateway Error" }, { status: 502 }); + } +} + +export const GET = proxyRequest; +export const POST = proxyRequest; +export const PUT = proxyRequest; +export const DELETE = proxyRequest; +export const PATCH = proxyRequest; diff --git a/my-portfolio/next.config.ts b/my-portfolio/next.config.ts index 3d4e7af..c811271 100644 --- a/my-portfolio/next.config.ts +++ b/my-portfolio/next.config.ts @@ -15,10 +15,6 @@ const nextConfig: NextConfig = { { source: "/api/media/:path*", destination: "http://localhost/api/media/:path*", // Nginx proxy - }, - { - source: "/api/admin/:path*", - destination: "http://localhost/api/admin/:path*", // Nginx proxy } ]; } diff --git a/my-portfolio/proxy.ts b/my-portfolio/proxy.ts index 23d696a..259c856 100644 --- a/my-portfolio/proxy.ts +++ b/my-portfolio/proxy.ts @@ -1,11 +1,17 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; + +export async function proxy(request: NextRequest) { + const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); + + if (!token) { + return NextResponse.redirect(new URL("/login", request.url)); + } -// Authentication is bypassed for local development — restore later -export function proxy(request: NextRequest) { return NextResponse.next(); } export const config = { - matcher: [], + matcher: ["/admin/:path*", "/dashboard/:path*"], }; diff --git a/my-portfolio/server/admin-service/index.js b/my-portfolio/server/admin-service/index.js index 57b5f2e..c877736 100644 --- a/my-portfolio/server/admin-service/index.js +++ b/my-portfolio/server/admin-service/index.js @@ -6,10 +6,19 @@ require('dotenv').config(); const app = express(); const docker = new Docker({ socketPath: '/var/run/docker.sock' }); const PORT = process.env.PORT || 4003; +const GATEWAY_SECRET = process.env.GATEWAY_SECRET || 'default_safe_secret_key_12345!'; app.use(cors()); app.use(express.json()); +app.use((req, res, next) => { + const secret = req.headers['x-gateway-secret']; + if (secret !== GATEWAY_SECRET) { + return res.status(403).json({ error: 'Forbidden: Invalid Gateway Secret' }); + } + next(); +}); + // List containers app.get('/containers', async (req, res) => { try {