feat(plan): wire hash-encoded API paths into B1 plan
This commit is contained in:
parent
31b092f231
commit
f0fbd01a79
@ -27,7 +27,8 @@
|
||||
| `web/app/layout.tsx` | Create (via frontend-design) | Root layout. |
|
||||
| `web/app/page.tsx` | Create (via frontend-design) | Scaffold-confirmation placeholder. |
|
||||
| `web/app/globals.css` | Create | `@import "tailwindcss";` |
|
||||
| `web/app/api/[...path]/route.ts` | Create | Catch-all GET/POST proxy to `api-server:3000`. |
|
||||
| `web/app/api/[...path]/route.ts` | Create | Catch-all GET/POST proxy that maps the public hash to the upstream path and forwards to `api-server:3000`. |
|
||||
| `web/lib/api-paths.ts` | Create | Hash → upstream-name mapping (single source of truth for both the Route Handler and future client code). |
|
||||
| `docker/web-next/Dockerfile` | Create | Multi-stage Node 22 alpine, standalone output. |
|
||||
| `docker-compose.yml` | Modify | Add `web-next` service. |
|
||||
| `docker-compose.override.yml` | Modify | Add `web-next` build directive. |
|
||||
@ -245,7 +246,8 @@ Generate two files for a Next.js 15 App Router project that uses Tailwind v4
|
||||
- Product name "CM Bot V2"
|
||||
- Literal text "cm-web-next scaffold" (operators grep for this)
|
||||
- One-line note that the real dashboard lands in B2
|
||||
- An obvious link to /api/acc/ for smoke-testing the proxy
|
||||
- An obvious link to /api/414322309db5c06d/ for smoke-testing the proxy
|
||||
(this is the SHA-256[:16] of "acc"; see web/lib/api-paths.ts)
|
||||
|
||||
Constraints:
|
||||
- Tailwind v4 utility classes only — no external font, image, or icon deps.
|
||||
@ -294,12 +296,60 @@ git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Catch-all Route Handler proxy to `api-server:3000`
|
||||
## Task 3: API path mapping + catch-all Route Handler proxy
|
||||
|
||||
**Files:**
|
||||
- Create: `web/lib/api-paths.ts`
|
||||
- Create: `web/app/api/[...path]/route.ts`
|
||||
|
||||
- [ ] **Step 1: Create the route handler directory**
|
||||
- [ ] **Step 1: Create `web/lib/api-paths.ts`**
|
||||
|
||||
```bash
|
||||
mkdir -p /home/yiekheng/projects/cm_bot_v2/web/lib
|
||||
```
|
||||
|
||||
Create `web/lib/api-paths.ts` with the precomputed hashes:
|
||||
|
||||
```typescript
|
||||
// SHA-256(endpoint).hex[:16]. Deterministic; no salt. Public-boundary only:
|
||||
// the upstream api-server still uses the readable names. See B1 spec.
|
||||
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 },
|
||||
};
|
||||
```
|
||||
|
||||
Verify the hashes (so future readers can double-check):
|
||||
|
||||
```bash
|
||||
.venv/bin/python3 -c "
|
||||
import hashlib
|
||||
for endpoint in ['acc', 'user', 'update-acc-data', 'update-user-data']:
|
||||
print(f'{endpoint:24s} -> {hashlib.sha256(endpoint.encode()).hexdigest()[:16]}')
|
||||
"
|
||||
```
|
||||
|
||||
Expected output (matches the constants above):
|
||||
|
||||
```
|
||||
acc -> 414322309db5c06d
|
||||
user -> 04f8996da763b7a9
|
||||
update-acc-data -> 982830e2982d95de
|
||||
update-user-data -> f1a25b37d8db494c
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the route handler directory**
|
||||
|
||||
```bash
|
||||
mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/api/\[...path\]
|
||||
@ -307,19 +357,26 @@ mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/api/\[...path\]
|
||||
|
||||
(The square brackets are Next.js's catch-all dynamic segment syntax. Shell-escape with backslashes.)
|
||||
|
||||
- [ ] **Step 2: Write the route handler**
|
||||
- [ ] **Step 3: Write the route handler**
|
||||
|
||||
Create `web/app/api/[...path]/route.ts`:
|
||||
|
||||
```typescript
|
||||
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 targetPath = "/" + path.join("/");
|
||||
const trailingSlash = request.nextUrl.pathname.endsWith("/") ? "/" : "";
|
||||
const target = `${API_BASE_URL}${targetPath}${trailingSlash}`;
|
||||
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,
|
||||
@ -362,7 +419,7 @@ export async function POST(
|
||||
}
|
||||
```
|
||||
|
||||
The error shape `{"error": <string>}` with HTTP 500 matches `cm_web_view.py`'s Flask proxy on exception.
|
||||
Mapped requests (`GET /api/414322309db5c06d/` etc.) resolve through `PUBLIC_TO_UPSTREAM` and forward to `api-server:3000/acc/` etc. Unmapped hashes return 404 — they're literally not routes we expose. Network/upstream errors return `{"error": <stringified-error>}` with HTTP 500, matching `cm_web_view.py`'s shape.
|
||||
|
||||
- [ ] **Step 3: Build to verify the route compiles**
|
||||
|
||||
@ -373,13 +430,22 @@ npm run build 2>&1 | tail -25
|
||||
|
||||
Expected: route summary now includes `/api/[...path]` (or similar). No TS errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
- [ ] **Step 4: Build and verify the route compiles**
|
||||
|
||||
```bash
|
||||
cd /home/yiekheng/projects/cm_bot_v2/web && \
|
||||
npm run build 2>&1 | tail -25
|
||||
```
|
||||
|
||||
Expected: route summary now includes `/api/[...path]`. No TS errors. The `@/lib/api-paths` import resolves via the `paths` mapping in `tsconfig.json`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/yiekheng/projects/cm_bot_v2 && \
|
||||
git add web/app/api/\[...path\]/route.ts && \
|
||||
git add web/lib/api-paths.ts web/app/api/\[...path\]/route.ts && \
|
||||
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
|
||||
commit -m "feat(web): add catch-all Route Handler proxy to api-server:3000"
|
||||
commit -m "feat(web): hash-encoded API paths + catch-all Route Handler proxy"
|
||||
```
|
||||
|
||||
---
|
||||
@ -779,10 +845,14 @@ Expected: hits in the response. Open `http://localhost:8010/` in a browser — p
|
||||
- [ ] **Step 3: API proxy parity (GET)**
|
||||
|
||||
```bash
|
||||
diff <(curl -s http://localhost:8000/api/acc/) <(curl -s http://localhost:8010/api/acc/) && echo "GET parity OK"
|
||||
# Old Flask: /api/acc/. New Next.js: /api/<sha256("acc")[:16]>/.
|
||||
diff \
|
||||
<(curl -s http://localhost:8000/api/acc/) \
|
||||
<(curl -s http://localhost:8010/api/414322309db5c06d/) \
|
||||
&& echo "GET parity OK"
|
||||
```
|
||||
|
||||
Expected: `GET parity OK`. Both go through to `api-server:3000/acc/` and return the same JSON.
|
||||
Expected: `GET parity OK`. Both go through to `api-server:3000/acc/` and return the same JSON. The hash mapping is the only thing in front of the upstream — same body, same status.
|
||||
|
||||
- [ ] **Step 4: API proxy parity (POST)**
|
||||
|
||||
@ -793,12 +863,20 @@ diff \
|
||||
http://localhost:8000/api/update-acc-data) \
|
||||
<(curl -s -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"username":"13c1000","password":"x","status":"","link":""}' \
|
||||
http://localhost:8010/api/update-acc-data) \
|
||||
http://localhost:8010/api/982830e2982d95de) \
|
||||
&& echo "POST parity OK"
|
||||
```
|
||||
|
||||
Expected: `POST parity OK`. Both POSTs round-trip through to `api-server:3000/update-acc-data`.
|
||||
|
||||
- [ ] **Step 4b: Unmapped hash returns 404**
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "code=%{http_code}\n" http://localhost:8010/api/deadbeefdeadbeef/
|
||||
```
|
||||
|
||||
Expected: `code=404`. Confirms the proxy is allowlist-based, not pass-through.
|
||||
|
||||
- [ ] **Step 5: Old `cm-web` still serves**
|
||||
|
||||
```bash
|
||||
@ -842,7 +920,10 @@ Expected: clean shutdown (no orphan complaints — `--remove-orphans` from the C
|
||||
| Next.js 15 + App Router + TypeScript + Tailwind v4 | Task 1 (configs) + Task 2 (app shell) |
|
||||
| `output: "standalone"`, `trailingSlash: true` in `next.config.ts` | Task 1 step 3 |
|
||||
| `frontend-design`-generated `layout.tsx` and `page.tsx` | Task 2 step 2 |
|
||||
| Catch-all proxy at `web/app/api/[...path]/route.ts` | Task 3 |
|
||||
| Hash-encoded API paths at the public boundary | Task 3 |
|
||||
| `web/lib/api-paths.ts` shared mapping for server + client | Task 3 step 1 |
|
||||
| Catch-all proxy at `web/app/api/[...path]/route.ts` (allowlist-based) | Task 3 step 3 |
|
||||
| Unmapped hash returns 404 (not pass-through) | Task 9 step 4b |
|
||||
| Multi-stage Dockerfile (Node 22 alpine, standalone output) | Task 4 |
|
||||
| `web-next` service in base compose, `${CM_WEB_NEXT_HOST_PORT:-8010}:3000` | Task 5 step 1 |
|
||||
| Build directive in override | Task 5 step 2 |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user