chore: clean up lint/type issues and tighten comment media URL validation

This commit is contained in:
Yusuf İpek
2026-02-23 16:06:43 +03:00
parent 609b33bbcb
commit d15e5192ac
19 changed files with 61 additions and 86 deletions
@@ -2,26 +2,12 @@ import Link from 'next/link';
import { notFound, redirect } from 'next/navigation';
import {
ArrowLeft,
Globe,
Lock,
UserPlus,
} from 'lucide-react';
import { GuestGate } from '@/components/guest-gate';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { ProjectContentClient } from './project-content-client';
function VisibilityIcon({ visibility }: { visibility: string }) {
switch (visibility) {
case 'PUBLIC':
return <Globe className="h-3.5 w-3.5" />;
case 'INVITE':
return <UserPlus className="h-3.5 w-3.5" />;
default:
return <Lock className="h-3.5 w-3.5" />;
}
}
function formatDuration(seconds: number | null): string {
if (!seconds) return '0:00';
const totalSeconds = Math.floor(seconds);
@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { ArrowLeft, Copy, Check, Loader2, UserPlus, Trash2, Share2, Globe, Lock, Mail, X } from 'lucide-react';
import { ArrowLeft, Copy, Check, Loader2, UserPlus, Share2, Globe, Lock, Mail, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
+1 -1
View File
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Bell, Send, Mail, CheckCircle2, AlertCircle, Loader2, ExternalLink, Globe } from 'lucide-react';
import { Bell, Send, Mail, CheckCircle2, AlertCircle, Loader2, Globe } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
+1 -1
View File
@@ -2,7 +2,7 @@ import { NextRequest } from 'next/server';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';
import { checkRateLimit, getClientIp, rateLimitHeaders, RATE_LIMIT_CONFIGS } from '@/lib/rate-limit';
import { apiErrors, successResponse, ErrorCode, withCacheControl } from '@/lib/api-response';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
export async function POST(request: NextRequest) {
try {
+2 -1
View File
@@ -107,7 +107,8 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
}
// Strip internal project data from response
const { version: _version, ...commentData } = comment;
const commentData = { ...comment } as Omit<typeof comment, 'version'> & { version?: unknown };
delete commentData.version;
const response = successResponse(commentData);
return withCacheControl(response, 'private, no-cache');
} catch (error) {
+6 -12
View File
@@ -1,12 +1,13 @@
import { NextRequest } from 'next/server';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { validateOptionalUrl } from '@/lib/validation';
import { rateLimit } from '@/lib/rate-limit';
import { notifyProjectOwner } from '@/lib/notifications';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
type RouteParams = { params: Promise<{ versionId: string }> };
const SAFE_IMAGE_PATH = /^\/api\/upload\/image\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i;
const SAFE_AUDIO_PATH = /^\/api\/upload\/audio\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i;
// GET /api/versions/[versionId]/comments
export async function GET(request: NextRequest, { params }: RouteParams) {
@@ -218,19 +219,12 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
return apiErrors.badRequest('Guest name is required for guest comments');
}
// Validate voice URL uses safe scheme (allow internal /api/ paths)
if (voiceUrl && !voiceUrl.startsWith('/api/')) {
const voiceUrlError = validateOptionalUrl(voiceUrl, 'Voice URL');
if (voiceUrlError) {
return apiErrors.badRequest(voiceUrlError);
}
if (voiceUrl && !SAFE_AUDIO_PATH.test(voiceUrl)) {
return apiErrors.badRequest('Voice URL must reference an uploaded audio file');
}
if (imageUrl && !imageUrl.startsWith('/api/')) {
const imageUrlError = validateOptionalUrl(imageUrl, 'Image URL');
if (imageUrlError) {
return apiErrors.badRequest(imageUrlError);
}
if (imageUrl && !SAFE_IMAGE_PATH.test(imageUrl)) {
return apiErrors.badRequest('Image URL must reference an uploaded image file');
}
const comment = await db.comment.create({