feat: implement R2 audio file management and rate limiting enhancements

- Add R2 client setup and audio upload functionality in lib/r2.ts.
- Create audio file cleanup functions in lib/r2-cleanup.ts to delete voice files associated with videos, projects, and workspaces.
- Enhance rate limiting in lib/rate-limit.ts with new action-specific limits and improved IP validation.
- Introduce a unified rate limit check function that returns a 429 response when limits are exceeded.
- Update package.json to include the AWS SDK for S3.
This commit is contained in:
Yusuf İpek
2026-02-07 12:27:40 +03:00
parent f240689e27
commit 296c5257a7
23 changed files with 1913 additions and 181 deletions
@@ -2,12 +2,16 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { ProjectMemberRole } from '@prisma/client';
import { rateLimit } from '@/lib/rate-limit';
type RouteParams = { params: Promise<{ projectId: string; memberId: string }> };
// PATCH /api/projects/[projectId]/members/[memberId] - Update member role
export async function PATCH(request: NextRequest, { params }: RouteParams) {
try {
const limited = await rateLimit(request, 'manage-member');
if (limited) return limited;
const session = await auth();
const { projectId, memberId } = await params;
@@ -63,6 +67,9 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
// DELETE /api/projects/[projectId]/members/[memberId] - Remove member
export async function DELETE(request: NextRequest, { params }: RouteParams) {
try {
const limited = await rateLimit(request, 'manage-member');
if (limited) return limited;
const session = await auth();
const { projectId, memberId } = await params;
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { ProjectMemberRole } from '@prisma/client';
import { rateLimit } from '@/lib/rate-limit';
type RouteParams = { params: Promise<{ projectId: string }> };
@@ -59,6 +60,9 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
// POST /api/projects/[projectId]/members - Invite a member
export async function POST(request: NextRequest, { params }: RouteParams) {
try {
const limited = await rateLimit(request, 'invite-member');
if (limited) return limited;
const session = await auth();
const { projectId } = await params;
+11
View File
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server';
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';
type RouteParams = { params: Promise<{ projectId: string }> };
@@ -117,6 +119,9 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
// PATCH /api/projects/[projectId] - Update a project
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 } = await params;
@@ -159,6 +164,9 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
// DELETE /api/projects/[projectId] - Delete a project
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 } = await params;
@@ -179,6 +187,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
);
}
// Clean up voice files from R2 before cascade delete removes comment rows
await cleanupProjectVoiceFiles(projectId);
await db.project.delete({ where: { id: projectId } });
return NextResponse.json({ success: true, message: 'Project deleted' });
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { ProjectMemberRole } from '@prisma/client';
import { rateLimit } from '@/lib/rate-limit';
import { cleanupVideoVoiceFiles } from '@/lib/r2-cleanup';
type RouteParams = { params: Promise<{ projectId: string; videoId: string }> };
@@ -65,6 +67,9 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
// 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;
@@ -122,6 +127,9 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
// 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;
@@ -152,6 +160,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
);
}
// Clean up voice files from R2 before cascade delete removes comment rows
await cleanupVideoVoiceFiles(videoId);
await db.video.delete({ where: { id: videoId } });
return NextResponse.json({ success: true, message: 'Video deleted' });
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { ProjectMemberRole } from '@prisma/client';
import { validateUrl, validateOptionalUrl } from '@/lib/validation';
import { rateLimit } from '@/lib/rate-limit';
type RouteParams = { params: Promise<{ projectId: string; videoId: string }> };
@@ -54,6 +55,9 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
// POST /api/projects/[projectId]/videos/[videoId]/versions - Add a new version
export async function POST(request: NextRequest, { params }: RouteParams) {
try {
const limited = await rateLimit(request, 'create-version');
if (limited) return limited;
const session = await auth();
const { projectId, videoId } = await params;
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { ProjectMemberRole } from '@prisma/client';
import { validateUrl, validateOptionalUrl } from '@/lib/validation';
import { rateLimit } from '@/lib/rate-limit';
type RouteParams = { params: Promise<{ projectId: string }> };
@@ -57,6 +58,9 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
// POST /api/projects/[projectId]/videos - Add a new video to the project
export async function POST(request: NextRequest, { params }: RouteParams) {
try {
const limited = await rateLimit(request, 'create-video');
if (limited) return limited;
const session = await auth();
const { projectId } = await params;
+4
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { ProjectVisibility } from '@prisma/client';
import { rateLimit } from '@/lib/rate-limit';
// GET /api/projects - List all projects for the authenticated user
export async function GET(request: NextRequest) {
@@ -75,6 +76,9 @@ export async function GET(request: NextRequest) {
// POST /api/projects - Create a new project
export async function POST(request: NextRequest) {
try {
const limited = await rateLimit(request, 'create-project');
if (limited) return limited;
const session = await auth();
if (!session?.user?.id) {