mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
fix: address security vulnerabilities and add image attachments
- Fix type confusion vulnerability in comment content updates - Validate pagination offsets to prevent negative values - Validate timestamp is a valid number before parsing - Exclude guestEmail from comment API responses for privacy - Fix TypeScript error in audio upload route - Add image attachment support for comments with upload API - Update admin dashboard to track image attachments - Rename cleanup functions to handle both voice and image media
This commit is contained in:
@@ -3,7 +3,7 @@ import { db } from '@/lib/db';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { ProjectMemberRole, ProjectVisibility } from '@prisma/client';
|
||||
import { rateLimit } from '@/lib/rate-limit';
|
||||
import { cleanupProjectVoiceFiles } from '@/lib/r2-cleanup';
|
||||
import { cleanupProjectMediaFiles } from '@/lib/r2-cleanup';
|
||||
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
|
||||
|
||||
type RouteParams = { params: Promise<{ projectId: string }> };
|
||||
@@ -52,11 +52,11 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const session = await auth();
|
||||
const { projectId } = await params;
|
||||
|
||||
|
||||
// Parse pagination params
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);
|
||||
const offset = parseInt(searchParams.get('offset') || '0');
|
||||
const offset = Math.max(0, parseInt(searchParams.get('offset') || '0'));
|
||||
|
||||
const project = await db.project.findUnique({
|
||||
where: { id: projectId },
|
||||
@@ -197,7 +197,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
}
|
||||
|
||||
// Clean up voice files from R2 before cascade delete removes comment rows
|
||||
await cleanupProjectVoiceFiles(projectId);
|
||||
await cleanupProjectMediaFiles(projectId);
|
||||
|
||||
await db.project.delete({ where: { id: projectId } });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { db } from '@/lib/db';
|
||||
import { auth, checkProjectAccess } from '@/lib/auth';
|
||||
import { ProjectMemberRole, WorkspaceMemberRole } from '@prisma/client';
|
||||
import { rateLimit } from '@/lib/rate-limit';
|
||||
import { cleanupVideoVoiceFiles } from '@/lib/r2-cleanup';
|
||||
import { cleanupVideoMediaFiles } from '@/lib/r2-cleanup';
|
||||
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
|
||||
|
||||
type RouteParams = { params: Promise<{ projectId: string; videoId: string }> };
|
||||
@@ -18,7 +18,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
// Parse query params for pagination and options
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const commentLimit = Math.min(parseInt(searchParams.get('commentLimit') || '50'), 100);
|
||||
const commentOffset = parseInt(searchParams.get('commentOffset') || '0');
|
||||
const commentOffset = Math.max(0, parseInt(searchParams.get('commentOffset') || '0'));
|
||||
const includeReplies = searchParams.get('includeReplies') === 'true';
|
||||
|
||||
const video = await db.video.findFirst({
|
||||
@@ -32,20 +32,54 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
orderBy: { timestamp: 'asc' },
|
||||
skip: commentOffset,
|
||||
take: commentLimit,
|
||||
include: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
timestamp: true,
|
||||
timestampEnd: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
isResolved: true,
|
||||
resolvedAt: true,
|
||||
voiceUrl: true,
|
||||
voiceDuration: true,
|
||||
imageUrl: true,
|
||||
parentId: true,
|
||||
authorId: true,
|
||||
tagId: true,
|
||||
versionId: true,
|
||||
guestName: true,
|
||||
// guestEmail excluded for privacy
|
||||
author: { select: { id: true, name: true, image: true } },
|
||||
tag: { select: { id: true, name: true, color: true } },
|
||||
...(includeReplies ? {
|
||||
replies: {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
timestamp: true,
|
||||
timestampEnd: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
isResolved: true,
|
||||
resolvedAt: true,
|
||||
voiceUrl: true,
|
||||
voiceDuration: true,
|
||||
imageUrl: true,
|
||||
parentId: true,
|
||||
authorId: true,
|
||||
tagId: true,
|
||||
versionId: true,
|
||||
guestName: true,
|
||||
// guestEmail excluded for privacy
|
||||
author: { select: { id: true, name: true, image: true } },
|
||||
tag: { select: { id: true, name: true, color: true } },
|
||||
},
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
where: { parentId: null }, // Only top-level comments
|
||||
where: { parentId: null },
|
||||
},
|
||||
_count: { select: { comments: true } },
|
||||
},
|
||||
@@ -194,7 +228,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
}
|
||||
|
||||
// Clean up voice files from R2 before cascade delete removes comment rows
|
||||
await cleanupVideoVoiceFiles(videoId);
|
||||
await cleanupVideoMediaFiles(videoId);
|
||||
|
||||
await db.video.delete({ where: { id: videoId } });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user