feat(04-04): slug resolution + multi-project client dashboard + slug edit

- validate-slug API route: resolves clients.slug for Edge middleware
- proxy.ts: slug-first resolution (D-06) — tries slug then falls back to token
- client-view.ts: complete rewrite — getClientWithProjectsByToken + getProjectView
  - No quote_items, no payment amounts in client API (CLAUDE.md security invariants)
- client/[token]/page.tsx: multi-project dashboard — 1 project = direct view,
  2+ projects = shadcn Tabs with project names (D-09/D-10)
- edit/page.tsx: slug field with link preview + unique constraint error handling
- actions.ts: updateClient now persists slug, redirects on success/slug_taken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 11:31:32 +02:00
parent 1d9fa10961
commit 5d785a1c1c
8 changed files with 562 additions and 175 deletions
+18 -7
View File
@@ -28,23 +28,34 @@ export async function proxy(request: NextRequest) {
return NextResponse.next();
}
// ── CLIENT TOKEN GUARD ───────────────────────────────────────────────────
// ── CLIENT TOKEN/SLUG GUARD ──────────────────────────────────────────────
if (pathname.startsWith("/client/")) {
const tokenMatch = pathname.match(/^\/client\/([a-zA-Z0-9_-]+)/);
if (!tokenMatch) {
const slugOrTokenMatch = pathname.match(/^\/client\/([a-zA-Z0-9_-]+)/);
if (!slugOrTokenMatch) {
return NextResponse.rewrite(new URL("/not-found", request.url));
}
const clientToken = tokenMatch[1];
const slugOrToken = slugOrTokenMatch[1];
try {
// Call internal Node.js API route — Edge middleware cannot use postgres-js directly
// postgres-js requires Node.js net/tls which are unavailable in the Edge runtime
const validateUrl = new URL(
`/api/internal/validate-token?token=${encodeURIComponent(clientToken)}`,
// Try slug first (D-06) — user-friendly slugs before token fallback
const validateSlugUrl = new URL(
`/api/internal/validate-slug?slug=${encodeURIComponent(slugOrToken)}`,
request.url
);
const res = await fetch(validateUrl.toString());
let res = await fetch(validateSlugUrl.toString());
// If slug not found, fall back to token validation (existing links continue to work)
if (!res.ok) {
const validateTokenUrl = new URL(
`/api/internal/validate-token?token=${encodeURIComponent(slugOrToken)}`,
request.url
);
res = await fetch(validateTokenUrl.toString());
}
if (!res.ok) {
return NextResponse.rewrite(new URL("/not-found", request.url));