mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
fix(api): add rate limiting, file size validation, and timeout handling
- Add Content-Length header check for early file size validation on audio upload - Add rate limiting (60 req/min) to public watch endpoint - Add 10-second timeout with AbortController for YouTube and Vimeo oEmbed requests - Add automatic rate limit cleanup interval for self-hosted servers - Fix null check for comment.replies in video page content - Add checkWorkspaceAccess helper for workspace authorization
This commit is contained in:
+2
-1
@@ -44,4 +44,5 @@ next-env.d.ts
|
||||
|
||||
# Progress (Internal Tracking)
|
||||
PROGRESS.md
|
||||
Optimization.md
|
||||
Optimization.md
|
||||
.kilocode
|
||||
@@ -10,6 +10,15 @@ const ALLOWED_TYPES = ['audio/webm', 'audio/ogg', 'audio/mp4', 'audio/mpeg', 'au
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
// Check Content-Length header BEFORE loading the file
|
||||
const contentLength = request.headers.get('content-length');
|
||||
if (contentLength) {
|
||||
const fileSize = parseInt(contentLength, 10);
|
||||
if (isNaN(fileSize) || fileSize > MAX_FILE_SIZE) {
|
||||
return apiErrors.badRequest('File too large. Maximum size is 10MB.');
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
const limited = await rateLimit(request, 'voice-upload');
|
||||
if (limited) return limited;
|
||||
@@ -27,6 +36,7 @@ export async function POST(request: Request) {
|
||||
return apiErrors.badRequest('No audio file provided');
|
||||
}
|
||||
|
||||
// Double-check file size (defense in depth - Content-Length can be spoofed)
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return apiErrors.badRequest('File too large. Maximum size is 10MB.');
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@ import { NextRequest } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { auth, checkProjectAccess } from '@/lib/auth';
|
||||
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
|
||||
import { rateLimit } from '@/lib/rate-limit';
|
||||
|
||||
type RouteParams = { params: Promise<{ videoId: string }> };
|
||||
|
||||
// GET /api/watch/[videoId] - Public watch endpoint (no projectId needed)
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
// Rate limit: 60 requests per minute per IP for public watch endpoint
|
||||
const limited = await rateLimit(request, 'watch', { windowMs: 60 * 1000, maxRequests: 60 });
|
||||
if (limited) return limited;
|
||||
|
||||
const session = await auth();
|
||||
const { videoId } = await params;
|
||||
|
||||
|
||||
@@ -2224,7 +2224,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
||||
)}
|
||||
</div>
|
||||
|
||||
{comment.replies.length > 0 && (
|
||||
{comment.replies && comment.replies.length > 0 && (
|
||||
<div className="mt-3 pl-3 border-l-2 space-y-2">
|
||||
{comment.replies.map((reply) => {
|
||||
const replyAuthor =
|
||||
|
||||
+30
@@ -125,3 +125,33 @@ export async function checkProjectAccess(
|
||||
canDelete,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to check workspace access
|
||||
export async function checkWorkspaceAccess(
|
||||
workspace: { id: string; ownerId: string },
|
||||
userId: string | undefined
|
||||
) {
|
||||
const isOwner = userId === workspace.ownerId;
|
||||
|
||||
// Get workspace membership
|
||||
const workspaceMember = userId
|
||||
? await db.workspaceMember.findUnique({
|
||||
where: { workspaceId_userId: { workspaceId: workspace.id, userId } },
|
||||
})
|
||||
: null;
|
||||
const isMember = !!workspaceMember;
|
||||
const isAdmin = workspaceMember?.role === WorkspaceMemberRole.ADMIN;
|
||||
|
||||
const hasAccess = isOwner || isMember;
|
||||
const canEdit = isOwner || isAdmin;
|
||||
const canDelete = isOwner;
|
||||
|
||||
return {
|
||||
isOwner,
|
||||
isMember,
|
||||
isAdmin,
|
||||
hasAccess,
|
||||
canEdit,
|
||||
canDelete,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,6 +160,14 @@ export async function cleanupRateLimits(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Start cleanup interval when the module is loaded (for self-hosted servers)
|
||||
// Cleanup runs every 5 minutes to remove expired rate limit entries
|
||||
if (typeof setInterval !== 'undefined') {
|
||||
setInterval(() => {
|
||||
cleanupRateLimits().catch(console.error);
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* One-call rate limit check that returns a 429 NextResponse if blocked, or null if allowed.
|
||||
* Use at the top of any API handler:
|
||||
|
||||
@@ -65,9 +65,14 @@ export const vimeoProvider: VideoProvider = {
|
||||
const cached = getCachedMetadata(cacheKey);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||
|
||||
const response = await fetch(
|
||||
`https://vimeo.com/api/oembed.json?url=https://vimeo.com/${videoId}`
|
||||
`https://vimeo.com/api/oembed.json?url=https://vimeo.com/${videoId}`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch video metadata');
|
||||
|
||||
@@ -64,9 +64,14 @@ export const youtubeProvider: VideoProvider = {
|
||||
// Using oEmbed API - no API key required
|
||||
// For production, you might want to use YouTube Data API for more data
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`
|
||||
`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch video metadata');
|
||||
|
||||
Reference in New Issue
Block a user