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/layout.tsx` | Create (via frontend-design) | Root layout. |
|
||||||
| `web/app/page.tsx` | Create (via frontend-design) | Scaffold-confirmation placeholder. |
|
| `web/app/page.tsx` | Create (via frontend-design) | Scaffold-confirmation placeholder. |
|
||||||
| `web/app/globals.css` | Create | `@import "tailwindcss";` |
|
| `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/web-next/Dockerfile` | Create | Multi-stage Node 22 alpine, standalone output. |
|
||||||
| `docker-compose.yml` | Modify | Add `web-next` service. |
|
| `docker-compose.yml` | Modify | Add `web-next` service. |
|
||||||
| `docker-compose.override.yml` | Modify | Add `web-next` build directive. |
|
| `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"
|
- Product name "CM Bot V2"
|
||||||
- Literal text "cm-web-next scaffold" (operators grep for this)
|
- Literal text "cm-web-next scaffold" (operators grep for this)
|
||||||
- One-line note that the real dashboard lands in B2
|
- 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:
|
Constraints:
|
||||||
- Tailwind v4 utility classes only — no external font, image, or icon deps.
|
- 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:**
|
**Files:**
|
||||||
|
- Create: `web/lib/api-paths.ts`
|
||||||
- Create: `web/app/api/[...path]/route.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
|
```bash
|
||||||
mkdir -p /home/yiekheng/projects/cm_bot_v2/web/app/api/\[...path\]
|
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.)
|
(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`:
|
Create `web/app/api/[...path]/route.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
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";
|
const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000";
|
||||||
|
|
||||||
async function forward(request: NextRequest, path: string[]): Promise<NextResponse> {
|
async function forward(
|
||||||
const targetPath = "/" + path.join("/");
|
request: NextRequest,
|
||||||
const trailingSlash = request.nextUrl.pathname.endsWith("/") ? "/" : "";
|
path: string[],
|
||||||
const target = `${API_BASE_URL}${targetPath}${trailingSlash}`;
|
): 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 = {
|
const init: RequestInit = {
|
||||||
method: request.method,
|
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**
|
- [ ] **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.
|
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
|
```bash
|
||||||
cd /home/yiekheng/projects/cm_bot_v2 && \
|
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' \
|
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)**
|
- [ ] **Step 3: API proxy parity (GET)**
|
||||||
|
|
||||||
```bash
|
```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)**
|
- [ ] **Step 4: API proxy parity (POST)**
|
||||||
|
|
||||||
@ -793,12 +863,20 @@ diff \
|
|||||||
http://localhost:8000/api/update-acc-data) \
|
http://localhost:8000/api/update-acc-data) \
|
||||||
<(curl -s -X POST -H 'Content-Type: application/json' \
|
<(curl -s -X POST -H 'Content-Type: application/json' \
|
||||||
-d '{"username":"13c1000","password":"x","status":"","link":""}' \
|
-d '{"username":"13c1000","password":"x","status":"","link":""}' \
|
||||||
http://localhost:8010/api/update-acc-data) \
|
http://localhost:8010/api/982830e2982d95de) \
|
||||||
&& echo "POST parity OK"
|
&& echo "POST parity OK"
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `POST parity OK`. Both POSTs round-trip through to `api-server:3000/update-acc-data`.
|
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**
|
- [ ] **Step 5: Old `cm-web` still serves**
|
||||||
|
|
||||||
```bash
|
```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) |
|
| 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 |
|
| `output: "standalone"`, `trailingSlash: true` in `next.config.ts` | Task 1 step 3 |
|
||||||
| `frontend-design`-generated `layout.tsx` and `page.tsx` | Task 2 step 2 |
|
| `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 |
|
| 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 |
|
| `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 |
|
| Build directive in override | Task 5 step 2 |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user