fix(types): unblock catch-all routes under stricter Next 15.5 typing + Phase 2B deps
Some checks failed
Build & Push Docker Images / lint (push) Successful in 1m26s
Build & Push Docker Images / build-and-push (push) Has been cancelled

Two changes bundled (build was failing on the type fix; deps came along
on the same branch).

1. RouteHandler / withAuth / withPermission are now generic over the
   route's params shape. Default stays `Record<string, string>` for the
   common `[id]`-style routes (no caller changes needed). Catch-all
   routes like `[...path]` declare their narrow shape via a type-arg:

       export const PATCH = withAuth<{ path: string[] }>(
         withPermission<{ path: string[] }>('files', 'manage_folders',
           async (req, ctx, params) => { /* params.path: string[] */ }
         ),
       );

   Without this, Next.js 15.5+'s stricter route-type checking rejected
   the build because the inferred `params: Promise<{ path: string[] }>`
   for `[...path]` doesn't satisfy `Promise<Record<string, string>>`.

   Updated `src/app/api/v1/files/folders/[...path]/route.ts` (the only
   catch-all in the tree right now) to use the new generic.

2. Phase 2B deps (within-major-jump where the API didn't actually break):
   - @pdfme/common, @pdfme/generator, @pdfme/schemas: 5.5.10 → 6.1.2
     (closes 3 mod XSS/SSRF/decompression-bomb advisories)
   - lucide-react: 0.460.0 → 1.14.0
   - sonner: 1.7.4 → 2.0.7
   - tailwind-merge: 2.6.1 → 3.5.0

Tests: 1185/1185 vitest. tsc clean. Local `next build` succeeds.

Reverted (deferred to a focused PR):
- @hookform/resolvers 5: Resolver<T> typing change requires per-form
  useForm migration
- eslint 10: incompatible with @rushstack/eslint-patch (pulled in by
  eslint-config-next)
- react-day-picker 10: ClassNames removed `table`; needs calendar.tsx
  migration
- zod 4: 94 type errors cascading through drizzle insert types; needs
  comprehensive migration

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 17:07:07 +02:00
parent fdb5beb81a
commit 2b1024ff7a
4 changed files with 77 additions and 1314 deletions

View File

@@ -31,9 +31,9 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.10.0",
"@pdfme/common": "^5.5.10",
"@pdfme/generator": "^5.5.10",
"@pdfme/schemas": "^5.5.10",
"@pdfme/common": "^6.1.2",
"@pdfme/generator": "^6.1.2",
"@pdfme/schemas": "^6.1.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
@@ -72,7 +72,7 @@
"iso-3166-2": "^1.0.0",
"jose": "^6.2.3",
"libphonenumber-js": "^1.12.43",
"lucide-react": "^0.460.0",
"lucide-react": "^1.14.0",
"mailparser": "^3.9.8",
"minio": "^8.0.7",
"next": "15.5.18",
@@ -93,8 +93,8 @@
"sharp": "^0.34.5",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"tesseract.js": "^7.0.0",
"vaul": "^1.1.2",

1327
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,13 +18,15 @@ function sanitizeFolderPath(raw: string): string {
.replace(/\/+/g, '/');
}
export const PATCH = withAuth(
withPermission('files', 'manage_folders', async (req, ctx, params) => {
// Catch-all route: Next.js 15.5+ infers `params: { path: string[] }` and
// enforces the handler signature matches. Pass the explicit type param
// so withAuth/withPermission narrow correctly.
type FolderPathParams = { path: string[] };
export const PATCH = withAuth<FolderPathParams>(
withPermission<FolderPathParams>('files', 'manage_folders', async (req, ctx, params) => {
try {
const pathSegments = params.path;
const currentPath = Array.isArray(pathSegments)
? (pathSegments as string[]).join('/')
: String(pathSegments);
const currentPath = params.path.join('/');
const body = await parseBody(req, renameFolderSchema);
const safeCurrent = sanitizeFolderPath(currentPath);
@@ -53,13 +55,10 @@ export const PATCH = withAuth(
}),
);
export const DELETE = withAuth(
withPermission('files', 'delete', async (req, ctx, params) => {
export const DELETE = withAuth<FolderPathParams>(
withPermission<FolderPathParams>('files', 'delete', async (req, ctx, params) => {
try {
const pathSegments = params.path;
const currentPath = Array.isArray(pathSegments)
? (pathSegments as string[]).join('/')
: String(pathSegments);
const currentPath = params.path.join('/');
const safePath = sanitizeFolderPath(currentPath);
if (!safePath) {

View File

@@ -44,11 +44,18 @@ export interface AuthContext {
userAgent: string;
}
export type RouteHandler<T = unknown> = (
req: NextRequest,
ctx: AuthContext,
params: Record<string, string>,
) => Promise<NextResponse<T>>;
/**
* Route params type. Defaults to `Record<string, string>` for the common
* `[id]`-style routes. Catch-all routes (`[...slug]`) need to override
* `TParams` so Next.js 15.5+'s stricter route-type checking accepts the
* exported handler against the inferred `{ slug: string[] }` shape.
*/
export type RouteParams = Record<string, string | string[]>;
export type RouteHandler<
TParams extends RouteParams = Record<string, string>,
T = unknown,
> = (req: NextRequest, ctx: AuthContext, params: TParams) => Promise<NextResponse<T>>;
// ─── deepMerge ───────────────────────────────────────────────────────────────
@@ -95,11 +102,11 @@ export function deepMerge(
* export const POST = withAuth(withPermission('clients', 'create', handler));
* ```
*/
export function withAuth(
handler: RouteHandler,
export function withAuth<TParams extends RouteParams = Record<string, string>>(
handler: RouteHandler<TParams>,
): (
req: NextRequest,
routeContext: { params: Promise<Record<string, string>> },
routeContext: { params: Promise<TParams> },
) => Promise<NextResponse> {
return async (req, routeContext) => {
// Mint or accept a request id BEFORE entering the ALS frame so every
@@ -286,11 +293,11 @@ export function requireSuperAdmin(ctx: AuthContext, attemptedAction = 'super_adm
* export const DELETE = withAuth(withPermission('clients', 'delete', handler));
* ```
*/
export function withPermission(
export function withPermission<TParams extends RouteParams = Record<string, string>>(
resource: keyof RolePermissions,
action: string,
handler: RouteHandler,
): RouteHandler {
handler: RouteHandler<TParams>,
): RouteHandler<TParams> {
return async (req, ctx, params) => {
if (!ctx.isSuperAdmin) {
const resourcePerms = ctx.permissions?.[resource] as Record<string, boolean> | undefined;