From 8ce1b622400cb022dab58a656e2b12d9c2e906ce Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Fri, 10 Apr 2026 18:55:18 +0200 Subject: [PATCH] docs(01): create phase 1 cleanup & fixes plans --- .planning/ROADMAP.md | 7 +- .../phases/01-cleanup-fixes/01-01-PLAN.md | 138 ++++++++++++ .../phases/01-cleanup-fixes/01-02-PLAN.md | 208 ++++++++++++++++++ 3 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/01-cleanup-fixes/01-01-PLAN.md create mode 100644 .planning/phases/01-cleanup-fixes/01-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4ebf629..5342861 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -27,7 +27,10 @@ 3. `package.json` ne contient ni `"latest"` ni `"*"` dans les deps 4. `siteConfig.seo.organization.aggregateRating.reviewCount` correspond a `testimonials.totalReviews` 5. 10 requetes POST rapides sur `/api/contact` → les dernieres sont rejetees (rate limit) -**Plans**: TBD +**Plans:** 2 plans +Plans: +- [ ] 01-01-PLAN.md — Delete static sitemap, pin deps, fix data inconsistencies +- [ ] 01-02-PLAN.md — Migrate Dockerfile to pnpm, add contact API rate limiting ### Phase 2: Content **Goal**: Un visiteur comprend immediatement que Killian est dev Hytale, peut voir les services/prix, et lire des temoignages clients @@ -71,7 +74,7 @@ | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Cleanup & Fixes | 0/? | Not started | - | +| 1. Cleanup & Fixes | 0/2 | Planning complete | - | | 2. Content | 0/? | Not started | - | | 3. SEO & i18n | 0/? | Not started | - | | 4. Ship | 0/? | Not started | - | diff --git a/.planning/phases/01-cleanup-fixes/01-01-PLAN.md b/.planning/phases/01-cleanup-fixes/01-01-PLAN.md new file mode 100644 index 0000000..5756e14 --- /dev/null +++ b/.planning/phases/01-cleanup-fixes/01-01-PLAN.md @@ -0,0 +1,138 @@ +--- +phase: 01-cleanup-fixes +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - public/sitemap.xml + - package.json + - app/data/site.ts +autonomous: true +requirements: + - FIX-01 + - FIX-04 + - FIX-05 +must_haves: + truths: + - "public/sitemap.xml no longer exists so @nuxtjs/sitemap serves the dynamic sitemap" + - "package.json has no 'latest' or '*' version specs" + - "reviewCount in site.ts matches totalReviews in testimonials.ts (both 10)" + - "Fiverr placeholder URLs '#' are replaced with the profile URL" + artifacts: + - path: "package.json" + provides: "Pinned vue and vue-router versions" + contains: "\"vue\": \"^3.5.0\"" + - path: "app/data/site.ts" + provides: "Consistent review data and valid Fiverr URLs" + contains: "reviewCount: '10'" + key_links: + - from: "app/data/site.ts" + to: "app/data/testimonials.ts" + via: "reviewCount must equal totalReviews" + pattern: "reviewCount.*10" +--- + + +Fix static sitemap conflict, pin dangerous dependency versions, and correct data inconsistencies. + +Purpose: Eliminate config conflicts and data integrity issues that affect SEO and build reproducibility. +Output: Clean package.json, no static sitemap, consistent site data. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/research/PITFALLS.md + + + + + + Task 1: Delete static sitemap and pin dependency versions + public/sitemap.xml, package.json + public/sitemap.xml, package.json + +1. Delete `public/sitemap.xml` entirely. This static file overrides the `@nuxtjs/sitemap` module dynamic route. Nitro serves `public/` files before server routes, so the module handler at `/sitemap.xml` is never reached while this file exists. + +2. In `package.json`, replace the two dangerous `"latest"` specs: + - Change `"vue": "latest"` to `"vue": "^3.5.0"` + - Change `"vue-router": "latest"` to `"vue-router": "^4.5.0"` + +Do NOT run `pnpm install` -- just update the version specs. The lockfile already has correct resolved versions. + + + bash -c "test ! -f public/sitemap.xml && echo 'sitemap deleted' || echo 'FAIL: sitemap exists'" && grep -c '"latest"' package.json | grep -q '^0$' && echo "no latest found" || echo "FAIL: latest still in package.json" + + +- `public/sitemap.xml` does not exist +- `grep '"latest"' package.json` returns zero matches +- `grep '"vue": "\\^3.5.0"' package.json` returns a match +- `grep '"vue-router": "\\^4.5.0"' package.json` returns a match + + Static sitemap removed, vue and vue-router pinned to caret ranges + + + + Task 2: Fix data inconsistencies in site.ts + app/data/site.ts, app/data/testimonials.ts + app/data/site.ts + +In `app/data/site.ts`, fix these inconsistencies: + +1. **reviewCount mismatch**: On line ~99, change `reviewCount: '50'` to `reviewCount: '10'`. The testimonials.ts file has `totalReviews: 10` -- these must match. Google penalises inflated aggregateRating claims in structured data. + +2. **Fiverr placeholder URLs**: On lines ~61 and ~67, two services have `url: '#'`: + - `id: 'telegram-bot'` (line ~61): change `url: '#'` to `url: 'https://www.fiverr.com/users/mr_kayjaydee'` (link to profile since no dedicated gig page exists) + - `id: 'website-development'` (line ~67): change `url: '#'` to `url: 'https://www.fiverr.com/users/mr_kayjaydee'` (same fallback) + +These are the Fiverr profile URL already defined at `fiverr.profileUrl` in the same file. + + + bash -c "grep -q \"reviewCount: '10'\" app/data/site.ts && echo 'reviewCount OK' || echo 'FAIL: reviewCount'" && bash -c "grep -c \"url: '#'\" app/data/site.ts | grep -q '^0$' && echo 'no placeholder URLs' || echo 'FAIL: placeholder URLs remain'" + + +- `grep "reviewCount: '10'" app/data/site.ts` returns a match +- `grep "reviewCount: '50'" app/data/site.ts` returns zero matches +- `grep "url: '#'" app/data/site.ts` returns zero matches +- Both telegram-bot and website-development services have `url: 'https://www.fiverr.com/users/mr_kayjaydee'` + + reviewCount matches totalReviews (10), Fiverr placeholder URLs replaced with profile URL + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Static assets vs server routes | `public/` files override Nitro server handlers | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-01-01 | Information Disclosure | aggregateRating JSON-LD | mitigate | Set reviewCount to actual value (10) to avoid Google penalty for inflated claims | +| T-01-02 | Tampering | package.json "latest" | mitigate | Pin to caret ranges to prevent unvetted major version upgrades | + + + +- `ls public/sitemap.xml` fails (file deleted) +- `grep '"latest"' package.json` returns 0 matches +- `grep "reviewCount: '10'" app/data/site.ts` returns 1 match +- `grep "url: '#'" app/data/site.ts` returns 0 matches + + + +Static sitemap removed, deps pinned, site data consistent with testimonials data. + + + +After completion, create `.planning/phases/01-cleanup-fixes/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-cleanup-fixes/01-02-PLAN.md b/.planning/phases/01-cleanup-fixes/01-02-PLAN.md new file mode 100644 index 0000000..787949a --- /dev/null +++ b/.planning/phases/01-cleanup-fixes/01-02-PLAN.md @@ -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" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/research/PITFALLS.md +@.planning/research/STACK.md + + + + + + Task 1: Migrate Dockerfile to pnpm + Dockerfile, package.json + Dockerfile + +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 + + + 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'" + + +- `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 + + Dockerfile uses pnpm exclusively with frozen lockfile for reproducible builds + + + + Task 2: Add rate limiting to contact API + server/api/contact.post.ts + server/plugins/rate-limit.ts + +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() + +// 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. + + + 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'" + + +- `server/plugins/rate-limit.ts` exists +- File contains `statusCode: 429` +- File contains check for `/api/contact` +- File contains `getRequestIP` +- File contains `Map` +- Rate limit is 3 requests per 60-second window + + Contact API rate-limited to 3 POST requests per IP per minute, 429 returned on excess + + + + + +## 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 | + + + +- `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 + + + +Dockerfile builds with pnpm, contact API rejects rapid submissions with 429. + + + +After completion, create `.planning/phases/01-cleanup-fixes/01-02-SUMMARY.md` +