feat(web): hide /api entirely — RSC + Server Actions instead
The Route Handler proxy and hash mapping are gone. Browser never hits a JSON endpoint: data reads happen in React Server Components fetching api-server:3000 server-side; mutations (B2) will use Next.js Server Actions. Zero public API surface to scrape or enumerate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a04d7ecc50
commit
ff99b1248a
@ -1,55 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { PUBLIC_TO_UPSTREAM } from "@/lib/api-paths";
|
|
||||||
|
|
||||||
const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000";
|
|
||||||
|
|
||||||
async function forward(
|
|
||||||
request: NextRequest,
|
|
||||||
path: string[],
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
const hash = path[0];
|
|
||||||
const route = PUBLIC_TO_UPSTREAM[hash];
|
|
||||||
if (!route) {
|
|
||||||
return NextResponse.json({ error: "Not Found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
const target = `${API_BASE_URL}/${route.upstream}${route.trailingSlash ? "/" : ""}`;
|
|
||||||
|
|
||||||
const init: RequestInit = {
|
|
||||||
method: request.method,
|
|
||||||
headers: {
|
|
||||||
"content-type":
|
|
||||||
request.headers.get("content-type") ?? "application/json",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
||||||
init.body = await request.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const upstream = await fetch(target, init);
|
|
||||||
const body = await upstream.text();
|
|
||||||
return new NextResponse(body, {
|
|
||||||
status: upstream.status,
|
|
||||||
headers: {
|
|
||||||
"content-type":
|
|
||||||
upstream.headers.get("content-type") ?? "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return NextResponse.json({ error: String(err) }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
ctx: { params: Promise<{ path: string[] }> },
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
return forward(request, (await ctx.params).path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
ctx: { params: Promise<{ path: string[] }> },
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
return forward(request, (await ctx.params).path);
|
|
||||||
}
|
|
||||||
@ -42,24 +42,25 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Smoke test */}
|
{/* Architecture note */}
|
||||||
<div className="mt-6 border-2 border-dashed border-black/50 bg-zinc-50 p-6 sm:p-8">
|
<div className="mt-6 border-2 border-dashed border-black/50 bg-zinc-50 p-6 sm:p-8">
|
||||||
<div className="mb-4 text-[10px] font-black uppercase tracking-[0.25em] text-zinc-400">
|
<div className="mb-4 text-[10px] font-black uppercase tracking-[0.25em] text-zinc-400">
|
||||||
Smoke Test
|
Architecture
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-zinc-800 sm:text-base">
|
<p className="text-sm text-zinc-800 sm:text-base">
|
||||||
Verify the API proxy reaches{" "}
|
No public{" "}
|
||||||
|
<code className="bg-black px-1.5 py-0.5 font-mono text-xs text-yellow-300">
|
||||||
|
/api
|
||||||
|
</code>{" "}
|
||||||
|
surface. Reads come from{" "}
|
||||||
|
<span className="font-bold">React Server Components</span> fetching{" "}
|
||||||
<code className="bg-black px-1.5 py-0.5 font-mono text-xs text-yellow-300">
|
<code className="bg-black px-1.5 py-0.5 font-mono text-xs text-yellow-300">
|
||||||
api-server:3000
|
api-server:3000
|
||||||
</code>
|
</code>{" "}
|
||||||
:
|
inside the docker network; mutations go through{" "}
|
||||||
|
<span className="font-bold">Server Actions</span>. The browser
|
||||||
|
never calls a JSON endpoint.
|
||||||
</p>
|
</p>
|
||||||
<a
|
|
||||||
href="/api/414322309db5c06d/"
|
|
||||||
className="mt-5 inline-block border-2 border-black bg-yellow-300 px-5 py-2.5 text-xs font-black uppercase tracking-[0.2em] text-black hover:bg-black hover:text-yellow-300"
|
|
||||||
>
|
|
||||||
GET /api/414322309db5c06d/ →
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer marker */}
|
{/* Footer marker */}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
// SHA-256(endpoint).hex[:16]. Deterministic; no salt. Public-boundary only:
|
|
||||||
// the upstream api-server still uses the readable names. See B1 spec.
|
|
||||||
//
|
|
||||||
// Verify with:
|
|
||||||
// python3 -c "import hashlib; print(hashlib.sha256(b'acc').hexdigest()[:16])"
|
|
||||||
export const API_PATHS = {
|
|
||||||
acc: "414322309db5c06d", // upstream: /acc/
|
|
||||||
user: "04f8996da763b7a9", // upstream: /user/
|
|
||||||
updateAcc: "982830e2982d95de", // upstream: /update-acc-data
|
|
||||||
updateUser: "f1a25b37d8db494c", // upstream: /update-user-data
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Reverse map for the Route Handler. Keys are the public path segment,
|
|
||||||
// values are { upstream: string; trailingSlash: boolean }.
|
|
||||||
export const PUBLIC_TO_UPSTREAM: Record<
|
|
||||||
string,
|
|
||||||
{ upstream: string; trailingSlash: boolean }
|
|
||||||
> = {
|
|
||||||
[API_PATHS.acc]: { upstream: "acc", trailingSlash: true },
|
|
||||||
[API_PATHS.user]: { upstream: "user", trailingSlash: true },
|
|
||||||
[API_PATHS.updateAcc]: { upstream: "update-acc-data", trailingSlash: false },
|
|
||||||
[API_PATHS.updateUser]: { upstream: "update-user-data", trailingSlash: false },
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user