docs(01): create phase 1 cleanup & fixes plans
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
---
|
||||
phase: 01-cleanup-fixes
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- Dockerfile
|
||||
- server/plugins/rate-limit.ts
|
||||
- server/api/contact.post.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- FIX-02
|
||||
- FIX-03
|
||||
- DEPLOY-01
|
||||
must_haves:
|
||||
truths:
|
||||
- "Dockerfile uses pnpm install --frozen-lockfile, not npm"
|
||||
- "Rapid POST requests to /api/contact are rejected with 429 after the limit"
|
||||
- "Docker build succeeds with pnpm"
|
||||
artifacts:
|
||||
- path: "Dockerfile"
|
||||
provides: "pnpm-based Docker build"
|
||||
contains: "pnpm install --frozen-lockfile"
|
||||
- path: "server/plugins/rate-limit.ts"
|
||||
provides: "In-memory rate limiting for contact API"
|
||||
contains: "429"
|
||||
key_links:
|
||||
- from: "server/plugins/rate-limit.ts"
|
||||
to: "/api/contact"
|
||||
via: "Nitro request hook filtering on path"
|
||||
pattern: "/api/contact"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Migrate Dockerfile from npm to pnpm and add rate limiting to the contact API endpoint.
|
||||
|
||||
Purpose: Fix build reproducibility (pnpm lockfile used in Docker) and protect against email flooding via unthrottled contact form submissions.
|
||||
Output: Working Dockerfile with pnpm, rate-limited contact endpoint.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/research/PITFALLS.md
|
||||
@.planning/research/STACK.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migrate Dockerfile to pnpm</name>
|
||||
<read_first>Dockerfile, package.json</read_first>
|
||||
<files>Dockerfile</files>
|
||||
<action>
|
||||
Replace the entire Dockerfile with:
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm via corepack
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# Copy manifests first for layer caching
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install all dependencies (including devDeps needed for build)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source and build
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
# Nuxt SSR bundles all server deps into .output/server/
|
||||
COPY --from=builder /app/.output /app/.output
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "/app/.output/server/index.mjs"]
|
||||
```
|
||||
|
||||
Key changes from original:
|
||||
- `corepack enable` + `corepack prepare pnpm@latest --activate` instead of relying on npm
|
||||
- `COPY package.json pnpm-lock.yaml ./` instead of `COPY package*.json ./`
|
||||
- `pnpm install --frozen-lockfile` instead of `npm ci`
|
||||
- `pnpm build` instead of `npm run build`
|
||||
- Explicit ENV vars for NODE_ENV, HOST, PORT in runtime stage
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bash -c "grep -q 'pnpm install --frozen-lockfile' Dockerfile && grep -q 'corepack enable' Dockerfile && grep -q 'pnpm build' Dockerfile && ! grep -q 'npm' Dockerfile && echo 'Dockerfile OK' || echo 'FAIL'"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep 'pnpm install --frozen-lockfile' Dockerfile` returns a match
|
||||
- `grep 'corepack enable' Dockerfile` returns a match
|
||||
- `grep 'pnpm build' Dockerfile` returns a match
|
||||
- `grep 'npm' Dockerfile` returns zero matches
|
||||
- `grep 'pnpm-lock.yaml' Dockerfile` returns a match
|
||||
</acceptance_criteria>
|
||||
<done>Dockerfile uses pnpm exclusively with frozen lockfile for reproducible builds</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add rate limiting to contact API</name>
|
||||
<read_first>server/api/contact.post.ts</read_first>
|
||||
<files>server/plugins/rate-limit.ts</files>
|
||||
<action>
|
||||
Create `server/plugins/rate-limit.ts` as a Nitro server plugin implementing in-memory IP-based rate limiting for the contact endpoint.
|
||||
|
||||
```typescript
|
||||
// server/plugins/rate-limit.ts
|
||||
const ipMap = new Map<string, { count: number; reset: number }>()
|
||||
|
||||
// Clean stale entries every 5 minutes to prevent memory leak
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
for (const [ip, entry] of ipMap) {
|
||||
if (entry.reset < now) ipMap.delete(ip)
|
||||
}
|
||||
}, 5 * 60 * 1000)
|
||||
|
||||
export default defineNitroPlugin((nitro) => {
|
||||
nitro.hooks.hook('request', (event) => {
|
||||
// Only rate-limit the contact POST endpoint
|
||||
if (event.method !== 'POST' || !event.path.startsWith('/api/contact')) return
|
||||
|
||||
const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'
|
||||
const now = Date.now()
|
||||
const window = 60_000 // 1 minute window
|
||||
const limit = 3 // max 3 requests per minute per IP
|
||||
|
||||
const entry = ipMap.get(ip)
|
||||
if (!entry || entry.reset < now) {
|
||||
ipMap.set(ip, { count: 1, reset: now + window })
|
||||
return
|
||||
}
|
||||
|
||||
entry.count++
|
||||
if (entry.count > limit) {
|
||||
throw createError({ statusCode: 429, message: 'Too many requests. Please try again later.' })
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
This uses Nitro's built-in `getRequestIP` and `createError` helpers (auto-imported in server context). The rate limit is 3 requests per IP per 60-second window. The 4th+ request within the window gets a 429 response.
|
||||
|
||||
The plugin hooks into ALL requests but filters to only `/api/contact` POST. No changes needed to `contact.post.ts` itself.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bash -c "test -f server/plugins/rate-limit.ts && grep -q '429' server/plugins/rate-limit.ts && grep -q '/api/contact' server/plugins/rate-limit.ts && echo 'rate-limit OK' || echo 'FAIL'"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `server/plugins/rate-limit.ts` exists
|
||||
- File contains `statusCode: 429`
|
||||
- File contains check for `/api/contact`
|
||||
- File contains `getRequestIP`
|
||||
- File contains `Map<string, { count: number; reset: number }>`
|
||||
- Rate limit is 3 requests per 60-second window
|
||||
</acceptance_criteria>
|
||||
<done>Contact API rate-limited to 3 POST requests per IP per minute, 429 returned on excess</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Client -> /api/contact | Untrusted POST from internet, potential spam/abuse |
|
||||
| Docker build -> production | Build must use same lockfile as dev to prevent supply chain drift |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-01-03 | Denial of Service | /api/contact | mitigate | In-memory rate limit: 3 req/min/IP via Nitro plugin, returns 429 on excess |
|
||||
| T-01-04 | Elevation of Privilege | Dockerfile npm vs pnpm | mitigate | Use pnpm --frozen-lockfile to ensure exact dependency resolution matches dev |
|
||||
| T-01-05 | Tampering | Rate limit bypass via IP spoofing | accept | X-Forwarded-For can be spoofed but acceptable risk for a portfolio contact form; reverse proxy (Docker/Cloudflare) controls the header |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `grep 'pnpm install --frozen-lockfile' Dockerfile` succeeds
|
||||
- `grep -c 'npm' Dockerfile` returns 0
|
||||
- `server/plugins/rate-limit.ts` exists with 429 response
|
||||
- Rate limit targets `/api/contact` POST only
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
Dockerfile builds with pnpm, contact API rejects rapid submissions with 429.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-cleanup-fixes/01-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user