diff --git a/app/(dashboard)/workspaces/[workspaceId]/page.tsx b/app/(dashboard)/workspaces/[workspaceId]/page.tsx
index d089cff..be26597 100644
--- a/app/(dashboard)/workspaces/[workspaceId]/page.tsx
+++ b/app/(dashboard)/workspaces/[workspaceId]/page.tsx
@@ -44,16 +44,27 @@ function formatRelativeTime(date: Date): string {
interface WorkspacePageProps {
params: Promise<{ workspaceId: string }>;
+ searchParams: Promise<{ page?: string }>;
}
-export default async function WorkspacePage({ params }: WorkspacePageProps) {
+export default async function WorkspacePage({ params, searchParams }: WorkspacePageProps) {
const session = await auth();
const { workspaceId } = await params;
+ const resolvedSearchParams = await searchParams;
+ const MAX_PAGE = 1000;
if (!session?.user?.id) {
redirect('/login');
}
+ const pageParam = resolvedSearchParams?.page;
+ const parsedPage = pageParam ? Number(pageParam) : 1;
+ const page = Number.isSafeInteger(parsedPage) && parsedPage > 0 && parsedPage <= MAX_PAGE
+ ? parsedPage
+ : 1;
+ const pageSize = 20;
+ const skip = (page - 1) * pageSize;
+
const workspace = await db.workspace.findUnique({
where: { id: workspaceId },
include: {
@@ -64,6 +75,8 @@ export default async function WorkspacePage({ params }: WorkspacePageProps) {
},
projects: {
orderBy: { updatedAt: 'desc' },
+ skip,
+ take: pageSize,
include: {
_count: { select: { videos: true, members: true } },
},
@@ -85,6 +98,8 @@ export default async function WorkspacePage({ params }: WorkspacePageProps) {
redirect('/workspaces');
}
+ const totalPages = Math.ceil(workspace._count.projects / pageSize);
+
return (
{/* Back & Header */}
@@ -143,42 +158,65 @@ export default async function WorkspacePage({ params }: WorkspacePageProps) {
{/* Projects Grid */}
{workspace.projects.length > 0 ? (
-
- {workspace.projects.map((project: (typeof workspace.projects)[number]) => (
-
-
-
-
-
-
- {project.name}
-
-
-
- {project.visibility.toLowerCase()}
-
-
-
- {project.description || 'No description'}
-
-
-
-
-
-
- {formatRelativeTime(project.updatedAt)}
-
-
-
- {project._count.members + 1}
-
- {project._count.videos} videos
-
-
-
-
- ))}
-
+ <>
+
+ {workspace.projects.map((project: (typeof workspace.projects)[number]) => (
+
+
+
+
+
+
+ {project.name}
+
+
+
+ {project.visibility.toLowerCase()}
+
+
+
+ {project.description || 'No description'}
+
+
+
+
+
+
+ {formatRelativeTime(project.updatedAt)}
+
+
+
+ {project._count.members + 1}
+
+ {project._count.videos} videos
+
+
+
+
+ ))}
+
+ {totalPages > 1 && (
+
+
+
+ Page {page} of {totalPages}
+
+
+
+ )}
+ >
) : (
diff --git a/app/api/projects/[projectId]/route.ts b/app/api/projects/[projectId]/route.ts
index b083140..8c61d64 100644
--- a/app/api/projects/[projectId]/route.ts
+++ b/app/api/projects/[projectId]/route.ts
@@ -12,11 +12,25 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
try {
const session = await auth();
const { projectId } = await params;
+ const MAX_LIMIT = 100;
+ const MAX_OFFSET = 10000;
// Parse pagination params
const searchParams = request.nextUrl.searchParams;
- const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);
- const offset = Math.max(0, parseInt(searchParams.get('offset') || '0'));
+ const limitParam = searchParams.get('limit');
+ const offsetParam = searchParams.get('offset');
+
+ const limitRaw = limitParam === null ? 20 : Number(limitParam);
+ if (!Number.isSafeInteger(limitRaw) || limitRaw < 1 || limitRaw > MAX_LIMIT) {
+ return apiErrors.badRequest('Invalid limit. Must be a positive integer between 1 and 100.');
+ }
+
+ const offset = offsetParam === null ? 0 : Number(offsetParam);
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > MAX_OFFSET) {
+ return apiErrors.badRequest('Invalid offset. Must be a non-negative integer up to 10000.');
+ }
+
+ const limit = limitRaw;
const project = await db.project.findUnique({
where: { id: projectId },
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
index dd3375d..1ebc8e2 100644
--- a/app/api/projects/route.ts
+++ b/app/api/projects/route.ts
@@ -10,16 +10,35 @@ import { DEFAULT_COMMENT_TAGS } from '@/lib/comment-tags';
export async function GET(request: NextRequest) {
try {
const session = await auth();
+ const MAX_LIMIT = 100;
+ const MAX_PAGE = 1000;
+ const MAX_OFFSET = 10000;
if (!session?.user?.id) {
return apiErrors.unauthorized();
}
const { searchParams } = new URL(request.url);
- const page = parseInt(searchParams.get('page') || '1');
- const limit = parseInt(searchParams.get('limit') || '10');
+ const pageParam = searchParams.get('page');
+ const limitParam = searchParams.get('limit');
const workspaceId = searchParams.get('workspaceId');
+
+ const pageRaw = pageParam === null ? 1 : Number(pageParam);
+ if (!Number.isSafeInteger(pageRaw) || pageRaw < 1 || pageRaw > MAX_PAGE) {
+ return apiErrors.badRequest('Invalid page. Must be a positive integer.');
+ }
+
+ const limitRaw = limitParam === null ? 10 : Number(limitParam);
+ if (!Number.isSafeInteger(limitRaw) || limitRaw < 1 || limitRaw > MAX_LIMIT) {
+ return apiErrors.badRequest('Invalid limit. Must be a positive integer between 1 and 100.');
+ }
+
+ const page = pageRaw;
+ const limit = limitRaw;
const skip = (page - 1) * limit;
+ if (!Number.isSafeInteger(skip) || skip > MAX_OFFSET) {
+ return apiErrors.badRequest('Invalid page range. Offset must be 10000 or less.');
+ }
// Build base filter: user is owner OR a member
const baseFilter: Record = {
diff --git a/app/api/workspaces/[workspaceId]/members/route.ts b/app/api/workspaces/[workspaceId]/members/route.ts
index cf49b54..ca77b63 100644
--- a/app/api/workspaces/[workspaceId]/members/route.ts
+++ b/app/api/workspaces/[workspaceId]/members/route.ts
@@ -12,11 +12,35 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
try {
const session = await auth();
const { workspaceId } = await params;
+ const MAX_LIMIT = 100;
+ const MAX_PAGE = 1000;
+ const MAX_OFFSET = 10000;
if (!session?.user?.id) {
return apiErrors.unauthorized();
}
+ const searchParams = request.nextUrl.searchParams;
+ const pageParam = searchParams.get('page');
+ const limitParam = searchParams.get('limit');
+
+ const pageRaw = pageParam === null ? 1 : Number(pageParam);
+ if (!Number.isSafeInteger(pageRaw) || pageRaw < 1 || pageRaw > MAX_PAGE) {
+ return apiErrors.badRequest('Invalid page. Must be a positive integer.');
+ }
+
+ const limitRaw = limitParam === null ? 20 : Number(limitParam);
+ if (!Number.isSafeInteger(limitRaw) || limitRaw < 1 || limitRaw > MAX_LIMIT) {
+ return apiErrors.badRequest('Invalid limit. Must be a positive integer between 1 and 100.');
+ }
+
+ const page = pageRaw;
+ const limit = limitRaw;
+ const skip = (page - 1) * limit;
+ if (!Number.isSafeInteger(skip) || skip > MAX_OFFSET) {
+ return apiErrors.badRequest('Invalid page range. Offset must be 10000 or less.');
+ }
+
const workspace = await db.workspace.findUnique({
where: { id: workspaceId },
include: {
@@ -35,13 +59,20 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
return apiErrors.forbidden('Access denied');
}
- const members = await db.workspaceMember.findMany({
- where: { workspaceId },
- include: {
- user: { select: { id: true, name: true, email: true, image: true } },
- },
- orderBy: { createdAt: 'asc' },
- });
+ const [members, total] = await Promise.all([
+ db.workspaceMember.findMany({
+ where: { workspaceId },
+ include: {
+ user: { select: { id: true, name: true, email: true, image: true } },
+ },
+ orderBy: { createdAt: 'asc' },
+ skip,
+ take: limit,
+ }),
+ db.workspaceMember.count({
+ where: { workspaceId },
+ }),
+ ]);
// Include the owner as well
const owner = await db.user.findUnique({
@@ -49,7 +80,16 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
select: { id: true, name: true, email: true, image: true },
});
- const response = successResponse({ members, owner });
+ const response = successResponse(
+ { members, owner },
+ 200,
+ {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit),
+ }
+ );
return withCacheControl(response, 'private, max-age=60, stale-while-revalidate=120');
} catch (error) {
console.error('Error fetching workspace members:', error);
diff --git a/app/api/workspaces/[workspaceId]/route.ts b/app/api/workspaces/[workspaceId]/route.ts
index 6ab178a..5a12de5 100644
--- a/app/api/workspaces/[workspaceId]/route.ts
+++ b/app/api/workspaces/[workspaceId]/route.ts
@@ -35,11 +35,29 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
try {
const session = await auth();
const { workspaceId } = await params;
+ const MAX_LIMIT = 100;
+ const MAX_OFFSET = 10000;
if (!session?.user?.id) {
return apiErrors.unauthorized();
}
+ const searchParams = request.nextUrl.searchParams;
+ const limitParam = searchParams.get('limit');
+ const offsetParam = searchParams.get('offset');
+
+ const limitRaw = limitParam === null ? 20 : Number(limitParam);
+ if (!Number.isSafeInteger(limitRaw) || limitRaw < 1 || limitRaw > MAX_LIMIT) {
+ return apiErrors.badRequest('Invalid limit. Must be a positive integer between 1 and 100.');
+ }
+
+ const offset = offsetParam === null ? 0 : Number(offsetParam);
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > MAX_OFFSET) {
+ return apiErrors.badRequest('Invalid offset. Must be a non-negative integer up to 10000.');
+ }
+
+ const limit = limitRaw;
+
const workspace = await db.workspace.findUnique({
where: { id: workspaceId },
include: {
@@ -51,6 +69,8 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
},
projects: {
orderBy: { updatedAt: 'desc' },
+ skip: offset,
+ take: limit,
include: {
_count: { select: { videos: true, members: true } },
},
diff --git a/app/api/workspaces/route.ts b/app/api/workspaces/route.ts
index 2216236..d5f77e7 100644
--- a/app/api/workspaces/route.ts
+++ b/app/api/workspaces/route.ts
@@ -5,30 +5,70 @@ import { rateLimit } from '@/lib/rate-limit';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
// GET /api/workspaces - List all workspaces for the authenticated user
-export async function GET() {
+export async function GET(request: NextRequest) {
try {
const session = await auth();
+ const MAX_LIMIT = 100;
+ const MAX_PAGE = 1000;
+ const MAX_OFFSET = 10000;
if (!session?.user?.id) {
return apiErrors.unauthorized();
}
- // Get workspaces where user is owner OR a member
- const workspaces = await db.workspace.findMany({
- where: {
- OR: [
- { ownerId: session.user.id },
- { members: { some: { userId: session.user.id } } },
- ],
- },
- include: {
- owner: { select: { id: true, name: true, image: true } },
- _count: { select: { projects: true, members: true } },
- },
- orderBy: { updatedAt: 'desc' },
- });
+ const searchParams = request.nextUrl.searchParams;
+ const pageParam = searchParams.get('page');
+ const limitParam = searchParams.get('limit');
- const response = successResponse({ workspaces });
+ const pageRaw = pageParam === null ? 1 : Number(pageParam);
+ if (!Number.isSafeInteger(pageRaw) || pageRaw < 1 || pageRaw > MAX_PAGE) {
+ return apiErrors.badRequest('Invalid page. Must be a positive integer.');
+ }
+
+ const limitRaw = limitParam === null ? 20 : Number(limitParam);
+ if (!Number.isSafeInteger(limitRaw) || limitRaw < 1 || limitRaw > MAX_LIMIT) {
+ return apiErrors.badRequest('Invalid limit. Must be a positive integer between 1 and 100.');
+ }
+
+ const page = pageRaw;
+ const limit = limitRaw;
+ const skip = (page - 1) * limit;
+ if (!Number.isSafeInteger(skip) || skip > MAX_OFFSET) {
+ return apiErrors.badRequest('Invalid page range. Offset must be 10000 or less.');
+ }
+
+ const where = {
+ OR: [
+ { ownerId: session.user.id },
+ { members: { some: { userId: session.user.id } } },
+ ],
+ };
+
+ // Get workspaces where user is owner OR a member
+ const [workspaces, total] = await Promise.all([
+ db.workspace.findMany({
+ where,
+ include: {
+ owner: { select: { id: true, name: true, image: true } },
+ _count: { select: { projects: true, members: true } },
+ },
+ orderBy: { updatedAt: 'desc' },
+ skip,
+ take: limit,
+ }),
+ db.workspace.count({ where }),
+ ]);
+
+ const response = successResponse(
+ { workspaces },
+ 200,
+ {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit),
+ }
+ );
return withCacheControl(response, 'private, max-age=60, stale-while-revalidate=120');
} catch (error) {
console.error('Error fetching workspaces:', error);