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:
+18
-7
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user