Fix photo and timeline submission bulletproofing

Implement rate limiting, validation, retry logic, and ban checking for photo and timeline submissions. This includes updates to `UppyPhotoSubmissionUpload.tsx` and `entitySubmissionHelpers.ts`.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-07 19:27:30 +00:00
parent 91a5b0e7dd
commit 6731e074a7
2 changed files with 380 additions and 146 deletions

View File

@@ -18,6 +18,9 @@ import { Camera, CheckCircle, AlertCircle, Info } from "lucide-react";
import { UppyPhotoSubmissionUploadProps } from "@/types/submissions";
import { withRetry } from "@/lib/retryHelpers";
import { logger } from "@/lib/logger";
import { breadcrumb } from "@/lib/errorBreadcrumbs";
import { checkSubmissionRateLimit, recordSubmissionAttempt } from "@/lib/submissionRateLimiter";
import { sanitizeErrorMessage } from "@/lib/errorSanitizer";
export function UppyPhotoSubmissionUpload({
onSubmissionComplete,
@@ -81,6 +84,54 @@ export function UppyPhotoSubmissionUpload({
setIsSubmitting(true);
try {
// ✅ Phase 4: Rate limiting check
const rateLimit = checkSubmissionRateLimit(user.id);
if (!rateLimit.allowed) {
const sanitizedMessage = sanitizeErrorMessage(rateLimit.reason || 'Rate limit exceeded');
logger.warn('[RateLimit] Photo submission blocked', {
userId: user.id,
reason: rateLimit.reason
});
throw new Error(sanitizedMessage);
}
recordSubmissionAttempt(user.id);
// ✅ Phase 4: Breadcrumb tracking
breadcrumb.userAction('Start photo submission', 'handleSubmit', {
photoCount: photos.length,
entityType,
entityId,
userId: user.id
});
// ✅ Phase 4: Ban check with retry
breadcrumb.apiCall('profiles', 'SELECT');
const profile = await withRetry(
async () => {
const { data, error } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', user.id)
.single();
if (error) throw error;
return data;
},
{ maxAttempts: 2 }
);
if (profile?.banned) {
throw new Error('Account suspended. Contact support for assistance.');
}
// ✅ Phase 4: Validate photos before processing
if (photos.some(p => !p.file)) {
throw new Error('All photos must have valid files');
}
breadcrumb.userAction('Upload images', 'handleSubmit', {
totalImages: photos.length
});
// Upload all photos that haven't been uploaded yet
const uploadedPhotos: PhotoWithCaption[] = [];
const photosToUpload = photos.filter((p) => p.file);
@@ -213,7 +264,24 @@ export function UppyPhotoSubmissionUpload({
setUploadProgress(null);
// ✅ Phase 4: Validate uploaded photos before DB insertion
breadcrumb.userAction('Validate photos', 'handleSubmit', {
uploadedCount: uploadedPhotos.length
});
const allPhotos = [...uploadedPhotos, ...photos.filter(p => !p.file)];
allPhotos.forEach((photo, index) => {
if (!photo.url) {
throw new Error(`Photo ${index + 1}: Missing URL`);
}
if (photo.uploadStatus === 'uploaded' && !photo.url.includes('/images/')) {
throw new Error(`Photo ${index + 1}: Invalid Cloudflare URL format`);
}
});
// Create submission records with retry logic
breadcrumb.apiCall('create_submission_with_items', 'RPC');
await withRetry(
async () => {
// Create content_submission record first