fix: 404 status on catch-all routes and admin gate on AI content API #157

Merged
ellamizuki merged 1 commit from fix/audit-soft-404-and-ai-content-auth into main 2026-09-09 01:10:38 +00:00
ellamizuki commented 2026-09-09 01:06:30 +00:00 (Migrated from github.com)

Summary

Full-project audit findings: two confirmed behavior bugs fixed, one low-severity API normalization fixed, each with a regression test.

Evidence / Reproduction (pre-fix, production)

Finding Probe Result
Catch-all soft 404 curl /nonexistent-page-xyz and /pt/nonexistent-page-xyz 200 with generic <title>Yuri Cunha</title>
AI content API open to the public curl -X POST /api/ai/content -d '{"action":"tags","content":"hello world test"}' (no auth) 200 with AI-generated tags at real provider cost
latest-post accepts unknown locale curl '/api/mascot/latest-post?locale=xx' 200 {"post":null} instead of falling back to en

Root cause

  1. [locale]/[...rest]/page.tsx rendered the localized not-found UI as a regular component, so unmatched paths got HTTP 200. Crawlers and caches treat that as valid, indexable content.
  2. /api/ai/content had no requireAdmin gate, unlike its sibling /api/admin/translate, and has no caller anywhere in the site — an orphaned admin-capable endpoint open to the world.
  3. latest-post treated any unknown ?locale= as "no posts" (|| 'en' only catches null/empty), silently disabling the mascot's new-post notification.

Fix

  1. Catch-all page now calls notFound() from next/navigation, rendering the same localized not-found UI with a real 404 status.
  2. POST /api/ai/content is gated by requireAdmin() (same pattern as /api/admin/translate). The metadata GET stays public.
  3. latest-post validates the requested locale against i18n.locales and falls back to the default locale.

Tests

  • catch-all-404.test.tsx: catch-all triggers notFound() (red before: the old component rendered without erroring).
  • ai-content-route.test.ts: new 401 case for unauthenticated POST; existing 429/400 cases updated to mock admin auth.
  • mascot-latest-post-route.test.ts: unknown locale falls back to en; every supported locale passes through untouched.

Validation

pnpm check:ci   # exit 0 (lint, typecheck, prettier, knip)
pnpm test:unit  # 63+ files, 276 tests passed
pnpm build      # 8 tasks successful

Pre-fix probes were run against production; post-fix behavior is covered by the regression tests above.

Residual risks

  • Newsletter subscribe still accepts any syntactically valid email without double opt-in (product decision, not addressed here; test subscription was created and unsubscribed during the audit).
  • Email templates support only en/pt; users of the other five locales receive transactional email in English (pre-existing scope limitation, flagged for a future decision).
## Summary Full-project audit findings: two confirmed behavior bugs fixed, one low-severity API normalization fixed, each with a regression test. ## Evidence / Reproduction (pre-fix, production) | Finding | Probe | Result | |---|---|---| | Catch-all soft 404 | `curl /nonexistent-page-xyz` and `/pt/nonexistent-page-xyz` | `200` with generic `<title>Yuri Cunha</title>` | | AI content API open to the public | `curl -X POST /api/ai/content -d '{"action":"tags","content":"hello world test"}'` (no auth) | `200` with AI-generated tags at real provider cost | | latest-post accepts unknown locale | `curl '/api/mascot/latest-post?locale=xx'` | `200 {"post":null}` instead of falling back to `en` | ## Root cause 1. `[locale]/[...rest]/page.tsx` rendered the localized not-found UI as a regular component, so unmatched paths got HTTP 200. Crawlers and caches treat that as valid, indexable content. 2. `/api/ai/content` had no `requireAdmin` gate, unlike its sibling `/api/admin/translate`, and has no caller anywhere in the site — an orphaned admin-capable endpoint open to the world. 3. `latest-post` treated any unknown `?locale=` as "no posts" (`|| 'en'` only catches null/empty), silently disabling the mascot's new-post notification. ## Fix 1. Catch-all page now calls `notFound()` from `next/navigation`, rendering the same localized not-found UI with a real `404` status. 2. `POST /api/ai/content` is gated by `requireAdmin()` (same pattern as `/api/admin/translate`). The metadata `GET` stays public. 3. `latest-post` validates the requested locale against `i18n.locales` and falls back to the default locale. ## Tests - `catch-all-404.test.tsx`: catch-all triggers `notFound()` (red before: the old component rendered without erroring). - `ai-content-route.test.ts`: new 401 case for unauthenticated POST; existing 429/400 cases updated to mock admin auth. - `mascot-latest-post-route.test.ts`: unknown locale falls back to `en`; every supported locale passes through untouched. ## Validation ``` pnpm check:ci # exit 0 (lint, typecheck, prettier, knip) pnpm test:unit # 63+ files, 276 tests passed pnpm build # 8 tasks successful ``` Pre-fix probes were run against production; post-fix behavior is covered by the regression tests above. ## Residual risks - Newsletter subscribe still accepts any syntactically valid email without double opt-in (product decision, not addressed here; test subscription was created and unsubscribed during the audit). - Email templates support only `en`/`pt`; users of the other five locales receive transactional email in English (pre-existing scope limitation, flagged for a future decision).
isyuricunha (Migrated from github.com) reviewed 2026-09-09 01:06:30 +00:00
vercel[bot] commented 2026-09-09 01:06:33 +00:00 (Migrated from github.com)

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
website-web Ready Ready Preview Sep 9, 2026 1:09am UTC
1 Skipped Deployment
Project Deployment Actions Updated
website-docs Ignored Ignored Preview Sep 9, 2026 1:09am UTC
[vc]: #37FluxxI7C2J05rRtEk8LdP1a5Nhz5hnxgb4as6C8xE=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3ZWJzaXRlLWRvY3MiLCJwcm9qZWN0SWQiOiJwcmpfMmVVMnNUTTBpZFk3MHJUYnNjQ0N0RXQ2TUZPdyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2RvY3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20veXVyaS1jdW5oYXMtcHJvamVjdHMvd2Vic2l0ZS1kb2NzL0o1cDRIOU03Uk5LcXpkNHptNnppd2VMWkNVUjUiLCJwcmV2aWV3VXJsIjoid2Vic2l0ZS1kb2NzLWdpdC1maXgtYXVkaXQtc29mdC00MDQtZmExYjhmLXl1cmktY3VuaGFzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX0seyJuYW1lIjoid2Vic2l0ZS13ZWIiLCJwcm9qZWN0SWQiOiJwcmpfYjZyRmhBQVBISVhSNzNjZW9kRTB5dmxCdzM4MiIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS95dXJpLWN1bmhhcy1wcm9qZWN0cy93ZWJzaXRlLXdlYi9EakVBZlVSU2ZRRnRkcVU0YmpTYXBrdkdYUDdjIiwicHJldmlld1VybCI6IndlYnNpdGUtd2ViLWdpdC1maXgtYXVkaXQtc29mdC00MDQtOWNjMmE1LXl1cmktY3VuaGFzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoid2Vic2l0ZS13ZWItZ2l0LWZpeC1hdWRpdC1zb2Z0LTQwNC05Y2MyYTUteXVyaS1jdW5oYXMtcHJvamVjdHMudmVyY2VsLmFwcCJ9fV19 The latest updates on your projects. Learn more about [Vercel for GitHub](https://vercel.link/github-learn-more). | Project | Deployment | Actions | Updated | | :--- | :----- | :------ | :------ | | <a href="https://vercel.com/yuri-cunhas-projects/website-web"><sup><img src="https://vercel.com/api/www/avatar?projectId=prj_b6rFhAAPHIXR73ceodE0yvlBw382&teamId=team_g087rqPZJa9s4DWPeZtQhxa9&s=32" width="16" height="16" align="middle" alt="" /></sup></a> [website-web](https://vercel.com/yuri-cunhas-projects/website-web) | ![Ready](https://vercel.com/static/status/ready.svg) [Ready](https://vercel.com/yuri-cunhas-projects/website-web/DjEAfURSfQFtdqU4bjSapkvGXP7c) | [Preview](https://website-web-git-fix-audit-soft-404-9cc2a5-yuri-cunhas-projects.vercel.app) | <relative-time datetime="2026-09-09T01:09:43.666Z">Sep 9, 2026 1:09am UTC</relative-time> | <details><summary>1 Skipped Deployment</summary> | Project | Deployment | Actions | Updated | | :--- | :----- | :------ | :------ | | <a href="https://vercel.com/yuri-cunhas-projects/website-docs"><sup><img src="https://vercel.com/api/www/avatar?projectId=prj_2eU2sTM0idY70rTbscCCtEt6MFOw&teamId=team_g087rqPZJa9s4DWPeZtQhxa9&s=32" width="16" height="16" align="middle" alt="" /></sup></a> [website-docs](https://vercel.com/yuri-cunhas-projects/website-docs) | ![Ignored](https://vercel.com/static/status/canceled.svg) [Ignored](https://vercel.com/yuri-cunhas-projects/website-docs/J5p4H9M7RNKqzd4zm6ziweLZCUR5) | [Preview](https://website-docs-git-fix-audit-soft-404-fa1b8f-yuri-cunhas-projects.vercel.app) | <relative-time datetime="2026-09-09T01:09:43.666Z">Sep 9, 2026 1:09am UTC</relative-time> | </details>
Sign in to join this conversation.
No description provided.