mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-12 01:46:08 +00:00
Add currentUserName to video API responses and session for display in comments. Also adds memoization for performance optimization and implements Page Visibility API to pause comment polling when tab is hidden.
211 lines
7.8 KiB
TypeScript
211 lines
7.8 KiB
TypeScript
import { NextRequest } from 'next/server';
|
|
import { revalidatePath } from 'next/cache';
|
|
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 { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
|
|
|
|
type RouteParams = { params: Promise<{ projectId: string; videoId: string }> };
|
|
|
|
// GET /api/projects/[projectId]/videos/[videoId]
|
|
export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
try {
|
|
const session = await auth();
|
|
const { projectId, videoId } = await params;
|
|
|
|
// 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 includeReplies = searchParams.get('includeReplies') === 'true';
|
|
|
|
const video = await db.video.findFirst({
|
|
where: { id: videoId, projectId },
|
|
include: {
|
|
project: true,
|
|
versions: {
|
|
where: { isActive: true },
|
|
orderBy: { versionNumber: 'desc' },
|
|
take: 1,
|
|
include: {
|
|
comments: {
|
|
orderBy: { timestamp: 'asc' },
|
|
skip: commentOffset,
|
|
take: commentLimit,
|
|
include: {
|
|
author: { select: { id: true, name: true, image: true } },
|
|
tag: { select: { id: true, name: true, color: true } },
|
|
...(includeReplies ? {
|
|
replies: {
|
|
orderBy: { createdAt: 'asc' },
|
|
include: {
|
|
author: { select: { id: true, name: true, image: true } },
|
|
tag: { select: { id: true, name: true, color: true } },
|
|
},
|
|
},
|
|
} : {}),
|
|
},
|
|
where: { parentId: null }, // Only top-level comments
|
|
},
|
|
_count: { select: { comments: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!video) {
|
|
return apiErrors.notFound('Video');
|
|
}
|
|
|
|
// Check access including workspace membership
|
|
const access = await checkProjectAccess(video.project, session?.user?.id);
|
|
|
|
if (!access.hasAccess) {
|
|
return apiErrors.forbidden('Access denied');
|
|
}
|
|
|
|
const response = successResponse({
|
|
...video,
|
|
isAuthenticated: !!session?.user?.id,
|
|
currentUserId: session?.user?.id || null,
|
|
currentUserName: session?.user?.name || null,
|
|
});
|
|
|
|
return withCacheControl(response, 'private, no-cache');
|
|
} catch (error) {
|
|
console.error('Error fetching video:', error);
|
|
return apiErrors.internalError('Failed to fetch video');
|
|
}
|
|
}
|
|
|
|
// PATCH /api/projects/[projectId]/videos/[videoId]
|
|
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
try {
|
|
const limited = await rateLimit(request, 'mutate');
|
|
if (limited) return limited;
|
|
|
|
const session = await auth();
|
|
const { projectId, videoId } = await params;
|
|
|
|
if (!session?.user?.id) {
|
|
return apiErrors.unauthorized();
|
|
}
|
|
|
|
const video = await db.video.findFirst({
|
|
where: { id: videoId, projectId },
|
|
include: {
|
|
project: {
|
|
include: {
|
|
members: { where: { userId: session.user.id } },
|
|
workspace: {
|
|
include: {
|
|
members: { where: { userId: session.user.id } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!video) {
|
|
return apiErrors.notFound('Video');
|
|
}
|
|
|
|
const isOwner = video.project.ownerId === session.user.id;
|
|
const membership = video.project.members[0];
|
|
const workspaceMembership = video.project.workspace.members[0];
|
|
const canEdit = isOwner ||
|
|
membership?.role === ProjectMemberRole.ADMIN ||
|
|
workspaceMembership?.role === WorkspaceMemberRole.ADMIN;
|
|
|
|
if (!canEdit) {
|
|
return apiErrors.forbidden('Access denied');
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { title, description, position } = body;
|
|
|
|
const updateData: Record<string, unknown> = {};
|
|
if (title !== undefined) updateData.title = title.trim();
|
|
if (description !== undefined) updateData.description = description?.trim() || null;
|
|
if (position !== undefined) updateData.position = position;
|
|
|
|
const updatedVideo = await db.video.update({
|
|
where: { id: videoId },
|
|
data: updateData,
|
|
include: {
|
|
versions: { orderBy: { versionNumber: 'desc' } },
|
|
_count: { select: { versions: true } },
|
|
},
|
|
});
|
|
|
|
const response = successResponse(updatedVideo);
|
|
return withCacheControl(response, 'private, no-store');
|
|
} catch (error) {
|
|
console.error('Error updating video:', error);
|
|
return apiErrors.internalError('Failed to update video');
|
|
}
|
|
}
|
|
|
|
// DELETE /api/projects/[projectId]/videos/[videoId]
|
|
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
try {
|
|
const limited = await rateLimit(request, 'mutate');
|
|
if (limited) return limited;
|
|
|
|
const session = await auth();
|
|
const { projectId, videoId } = await params;
|
|
|
|
if (!session?.user?.id) {
|
|
return apiErrors.unauthorized();
|
|
}
|
|
|
|
const video = await db.video.findFirst({
|
|
where: { id: videoId, projectId },
|
|
include: {
|
|
project: {
|
|
include: {
|
|
members: { where: { userId: session.user.id } },
|
|
workspace: {
|
|
include: {
|
|
members: { where: { userId: session.user.id } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!video) {
|
|
return apiErrors.notFound('Video');
|
|
}
|
|
|
|
const isOwner = video.project.ownerId === session.user.id;
|
|
const membership = video.project.members[0];
|
|
const workspaceMembership = video.project.workspace.members[0];
|
|
// Destructive actions limited to OWNER and ADMIN only
|
|
const canDelete = isOwner ||
|
|
membership?.role === ProjectMemberRole.ADMIN ||
|
|
workspaceMembership?.role === WorkspaceMemberRole.ADMIN;
|
|
|
|
if (!canDelete) {
|
|
return apiErrors.forbidden('Only project owner or admin can delete videos');
|
|
}
|
|
|
|
// Clean up voice files from R2 before cascade delete removes comment rows
|
|
await cleanupVideoVoiceFiles(videoId);
|
|
|
|
await db.video.delete({ where: { id: videoId } });
|
|
|
|
revalidatePath(`/projects/${projectId}`);
|
|
|
|
const response = successResponse({ message: 'Video deleted' });
|
|
return withCacheControl(response, 'private, no-store');
|
|
} catch (error) {
|
|
console.error('Error deleting video:', error);
|
|
return apiErrors.internalError('Failed to delete video');
|
|
}
|
|
}
|