v1.2.0:1 — upgrade to Next.js 15 / React 19
Closes the remaining P1: move off Next 14 onto the CVE-patched Next 15 line (15.5.x), eliminating the framework's RSC DoS/source-exposure advisories and the middleware-auth-bypass class that applied to the 14.x auth gate. App Router on Next 15 requires React 19, so react/react-dom move to 19.x in lockstep; lucide-react and next-themes bump to their React-19-compatible releases. The code surface was the Next 15 async-request-API change: params and searchParams are now Promises. All [id] route handlers (10 files) and the four server pages that read them now await the resolved value, using a uniform re-derive idiom that leaves handler bodies untouched. cookies()/ headers() were already awaited, so no other request-API changes were needed; all routes stay dynamic, so the uncached-by-default change is a no-op. next.config.js (static CSP) and the middleware matcher are unchanged. No schema, no API contract change, no data migration. Verified: tsc + lint clean, 209 tests pass, next build succeeds with the standalone bundle tracing the Prisma engine.
This commit is contained in:
@@ -12,8 +12,9 @@ import { activate } from '@/lib/ai/activateConfig';
|
||||
*/
|
||||
export async function POST(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ import { isCustomUrlProvider } from '@/lib/ai/providers';
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
@@ -54,8 +55,9 @@ const patchSchema = z.object({
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
@@ -122,8 +124,9 @@ export async function PATCH(
|
||||
|
||||
export async function DELETE(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
@@ -29,8 +30,9 @@ export async function GET(
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@ async function loadAndCheck(
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user)
|
||||
@@ -82,8 +83,9 @@ export async function PATCH(
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user)
|
||||
|
||||
@@ -22,8 +22,9 @@ import { z } from "zod";
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
@@ -100,8 +101,9 @@ const updateExerciseSchema = z.object({
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
@@ -166,8 +168,9 @@ export async function PATCH(
|
||||
*/
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
|
||||
@@ -22,8 +22,9 @@ const bodySchema = z.object({
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string; dayId: string } },
|
||||
context: { params: Promise<{ id: string; dayId: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
@@ -54,8 +54,9 @@ const patchSchema = z.object({
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
const program = await getProgramById(user.id, params.id);
|
||||
@@ -67,8 +68,9 @@ export async function GET(
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
@@ -183,8 +185,9 @@ export async function PATCH(
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } },
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
@@ -7,8 +7,9 @@ import { z } from "zod";
|
||||
// GET: Get workout by ID
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
@@ -75,8 +76,9 @@ const updateWorkoutSchema = z.object({
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
@@ -199,8 +201,9 @@ export async function PATCH(
|
||||
// DELETE: Delete workout
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
|
||||
@@ -26,8 +26,9 @@ const addSetsSchema = z.object({
|
||||
// POST: Add an exercise's sets to an existing workout
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
@@ -110,8 +111,9 @@ export async function POST(
|
||||
// DELETE: Remove all sets for a specific exercise from a workout
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const params = await context.params;
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
|
||||
@@ -25,11 +25,10 @@ export const dynamic = 'force-dynamic';
|
||||
* applied → "View applied program" link
|
||||
* failed → error + raw response details
|
||||
*/
|
||||
export default async function GenerationDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
export default async function GenerationDetailPage(props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) redirect('/auth/login');
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
export default async function ProgramDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
export default async function ProgramDetailPage(props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) redirect('/auth/login');
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@ export const metadata = {
|
||||
description: "Log a new workout",
|
||||
};
|
||||
|
||||
export default async function NewWorkoutPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { edit?: string };
|
||||
export default async function NewWorkoutPage(props: {
|
||||
searchParams: Promise<{ edit?: string }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
redirect("/auth/login");
|
||||
|
||||
@@ -8,7 +8,7 @@ import WorkoutsList from "@/components/workouts/WorkoutsList";
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
interface PageProps {
|
||||
searchParams: { q?: string; dateFrom?: string; dateTo?: string };
|
||||
searchParams: Promise<{ q?: string; dateFrom?: string; dateTo?: string }>;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
@@ -18,7 +18,8 @@ export const metadata = {
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function WorkoutsPage({ searchParams }: PageProps) {
|
||||
export default async function WorkoutsPage(props: PageProps) {
|
||||
const searchParams = await props.searchParams;
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
redirect("/auth/login");
|
||||
|
||||
Vendored
+2
-1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Generated
+644
-557
File diff suppressed because it is too large
Load Diff
@@ -22,12 +22,12 @@
|
||||
"autoprefixer": "^10.4.16",
|
||||
"bcrypt": "^6.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.0.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"lucide-react": "^1.18.0",
|
||||
"next": "^15.5.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.4.31",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"zod": "^3.22.4"
|
||||
@@ -35,11 +35,11 @@
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^14.2.35",
|
||||
"eslint-config-next": "^15.5.9",
|
||||
"prisma": "^5.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.2.2",
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('PATCH /api/ai/templates/[id]', () => {
|
||||
{ name: 'tampered' },
|
||||
'PATCH',
|
||||
),
|
||||
{ params: { id: builtin.id } },
|
||||
{ params: Promise.resolve({ id: builtin.id }) },
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
@@ -147,7 +147,7 @@ describe('PATCH /api/ai/templates/[id]', () => {
|
||||
{ name: 'admin-edited' },
|
||||
'PATCH',
|
||||
),
|
||||
{ params: { id: builtin.id } },
|
||||
{ params: Promise.resolve({ id: builtin.id }) },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const updated = await prisma.aIPromptTemplate.findUnique({
|
||||
@@ -178,7 +178,7 @@ describe('PATCH /api/ai/templates/[id]', () => {
|
||||
{ name: 'bob hacks' },
|
||||
'PATCH',
|
||||
),
|
||||
{ params: { id: aliceTpl.id } },
|
||||
{ params: Promise.resolve({ id: aliceTpl.id }) },
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
@@ -200,7 +200,7 @@ describe('DELETE /api/ai/templates/[id]', () => {
|
||||
getCurrentUserMock.mockResolvedValue(me);
|
||||
const res = await deleteTemplate(
|
||||
jsonReq(`http://x/api/ai/templates/${tpl.id}`, undefined, 'DELETE'),
|
||||
{ params: { id: tpl.id } },
|
||||
{ params: Promise.resolve({ id: tpl.id }) },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await prisma.aIPromptTemplate.count()).toBe(0);
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
it('returns 401 unauthenticated', async () => {
|
||||
getCurrentUserMock.mockResolvedValue(null);
|
||||
const res = await getExerciseDetail(req('http://x/api/exercises/anything'), {
|
||||
params: { id: 'anything' },
|
||||
params: Promise.resolve({ id: 'anything' }),
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
});
|
||||
getCurrentUserMock.mockResolvedValue(me);
|
||||
const res = await getExerciseDetail(req('http://x/api/exercises/' + exercise.id), {
|
||||
params: { id: exercise.id },
|
||||
params: Promise.resolve({ id: exercise.id }),
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
@@ -108,7 +108,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
getCurrentUserMock.mockResolvedValue(user);
|
||||
const res = await getExerciseDetail(
|
||||
req(`http://x/api/exercises/${exercise.id}?offset=0&limit=10`),
|
||||
{ params: { id: exercise.id } },
|
||||
{ params: Promise.resolve({ id: exercise.id }) },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
@@ -135,7 +135,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
const r1 = await (
|
||||
await getExerciseDetail(
|
||||
req(`http://x/api/exercises/${exercise.id}?offset=0&limit=10`),
|
||||
{ params: { id: exercise.id } },
|
||||
{ params: Promise.resolve({ id: exercise.id }) },
|
||||
)
|
||||
).json();
|
||||
expect(r1.history).toHaveLength(10);
|
||||
@@ -144,7 +144,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
const r2 = await (
|
||||
await getExerciseDetail(
|
||||
req(`http://x/api/exercises/${exercise.id}?offset=10&limit=10`),
|
||||
{ params: { id: exercise.id } },
|
||||
{ params: Promise.resolve({ id: exercise.id }) },
|
||||
)
|
||||
).json();
|
||||
expect(r2.history).toHaveLength(8); // 18 - 10
|
||||
@@ -168,7 +168,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
getCurrentUserMock.mockResolvedValue(user);
|
||||
const res = await getExerciseDetail(
|
||||
req(`http://x/api/exercises/${exercise.id}?offset=0&limit=25`),
|
||||
{ params: { id: exercise.id } },
|
||||
{ params: Promise.resolve({ id: exercise.id }) },
|
||||
);
|
||||
const body = await res.json();
|
||||
expect(body.history).toHaveLength(1);
|
||||
@@ -200,7 +200,7 @@ describe('GET /api/exercises/[id] (paginated history)', () => {
|
||||
getCurrentUserMock.mockResolvedValue(user);
|
||||
const res = await getExerciseDetail(
|
||||
req(`http://x/api/exercises/${exercise.id}`),
|
||||
{ params: { id: exercise.id } },
|
||||
{ params: Promise.resolve({ id: exercise.id }) },
|
||||
);
|
||||
const body = await res.json();
|
||||
expect(body.history).toHaveLength(4);
|
||||
|
||||
@@ -232,7 +232,7 @@ describe('GET /api/programs + GET /api/programs/[id]', () => {
|
||||
|
||||
const detail = await (
|
||||
await getProgram(jsonReq(`http://x/api/programs/${programId}`), {
|
||||
params: { id: programId },
|
||||
params: Promise.resolve({ id: programId }),
|
||||
})
|
||||
).json();
|
||||
expect(detail.weeks[0].days[0].exercises[0].exercise.name).toBe('Bench');
|
||||
@@ -259,7 +259,7 @@ describe('GET /api/programs + GET /api/programs/[id]', () => {
|
||||
getCurrentUserMock.mockResolvedValue(bob);
|
||||
const res = await getProgram(
|
||||
jsonReq(`http://x/api/programs/${aliceProg.id}`),
|
||||
{ params: { id: aliceProg.id } },
|
||||
{ params: Promise.resolve({ id: aliceProg.id }) },
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
@@ -318,7 +318,7 @@ describe('PATCH /api/programs/[id] (replace tree)', () => {
|
||||
},
|
||||
'PATCH',
|
||||
),
|
||||
{ params: { id: created.id } },
|
||||
{ params: Promise.resolve({ id: created.id }) },
|
||||
);
|
||||
expect(patchRes.status).toBe(200);
|
||||
|
||||
@@ -377,7 +377,7 @@ describe('DELETE /api/programs/[id]', () => {
|
||||
|
||||
const res = await deleteProgram(
|
||||
jsonReq(`http://x/api/programs/${created.id}`, undefined, 'DELETE'),
|
||||
{ params: { id: created.id } },
|
||||
{ params: Promise.resolve({ id: created.id }) },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await prisma.programWeek.count()).toBe(0);
|
||||
@@ -427,7 +427,7 @@ describe('POST /api/programs/[id]/days/[dayId]/start', () => {
|
||||
`http://x/api/programs/${created.id}/days/${day!.id}/start`,
|
||||
{},
|
||||
),
|
||||
{ params: { id: created.id, dayId: day!.id } },
|
||||
{ params: Promise.resolve({ id: created.id, dayId: day!.id }) },
|
||||
);
|
||||
expect(startRes.status).toBe(201);
|
||||
const workout = await startRes.json();
|
||||
@@ -485,7 +485,7 @@ describe('POST /api/programs/[id]/days/[dayId]/start', () => {
|
||||
jsonReq(
|
||||
`http://x/api/programs/${aliceProg.id}/days/${aliceDay!.id}/start`,
|
||||
),
|
||||
{ params: { id: aliceProg.id, dayId: aliceDay!.id } },
|
||||
{ params: Promise.resolve({ id: aliceProg.id, dayId: aliceDay!.id }) },
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user