diff --git a/.gitignore b/.gitignore
index dd85413..e870033 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,4 +44,5 @@ next-env.d.ts
# Progress (Internal Tracking)
PROGRESS.md
-Optimization.md
\ No newline at end of file
+Optimization.md
+.kilocode
\ No newline at end of file
diff --git a/app/api/upload/audio/route.ts b/app/api/upload/audio/route.ts
index fe86f39..7c2cffd 100644
--- a/app/api/upload/audio/route.ts
+++ b/app/api/upload/audio/route.ts
@@ -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.');
}
diff --git a/app/api/watch/[videoId]/route.ts b/app/api/watch/[videoId]/route.ts
index 10cda16..e94f238 100644
--- a/app/api/watch/[videoId]/route.ts
+++ b/app/api/watch/[videoId]/route.ts
@@ -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;
diff --git a/components/video-page-content.tsx b/components/video-page-content.tsx
index 89d3f84..ddb22a3 100644
--- a/components/video-page-content.tsx
+++ b/components/video-page-content.tsx
@@ -2224,7 +2224,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
)}
- {comment.replies.length > 0 && (
+ {comment.replies && comment.replies.length > 0 && (
{comment.replies.map((reply) => {
const replyAuthor =
diff --git a/lib/auth.ts b/lib/auth.ts
index dad77bf..8e8afa4 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -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,
+ };
+}
diff --git a/lib/rate-limit.ts b/lib/rate-limit.ts
index 0abff8a..cecadda 100644
--- a/lib/rate-limit.ts
+++ b/lib/rate-limit.ts
@@ -160,6 +160,14 @@ export async function cleanupRateLimits(): Promise {
}
}
+// 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:
diff --git a/lib/video-providers/vimeo.ts b/lib/video-providers/vimeo.ts
index afd0724..f517ab7 100644
--- a/lib/video-providers/vimeo.ts
+++ b/lib/video-providers/vimeo.ts
@@ -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');
diff --git a/lib/video-providers/youtube.ts b/lib/video-providers/youtube.ts
index 8ab2bf5..5054f17 100644
--- a/lib/video-providers/youtube.ts
+++ b/lib/video-providers/youtube.ts
@@ -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');