File Upload API
Ring Platform's file upload system provides secure file handling for entities, opportunities, and user profiles.
Overviewβ
The upload system supports:
- Multiple File Types: Images, documents, and media files
- Secure Storage: Vercel Blob storage with secure URLs
- Size Validation: Configurable file size limits
- Type Validation: Allowed file type restrictions
- Authenticated Access: User-based upload permissions
- Entity Association: Link uploads to specific entities or opportunities
Base URLβ
Development: http://localhost:3000/api
Production: https://ring.ck.ua/api
Authenticationβ
All upload endpoints require authentication via NextAuth.js session:
headers: {
'Cookie': 'next-auth.session-token=<token>'
}
API Endpointsβ
General File Uploadβ
Upload files to Vercel Blob storage with automatic metadata handling.
Endpoint: POST /api/upload
Content Type: multipart/form-data
Request Body:
file
(File, required): The file to uploadtype
(string, optional): Upload context type ('entity', 'opportunity', 'profile', 'general')entityId
(string, optional): Associated entity ID for contextopportunityId
(string, optional): Associated opportunity ID for context
Response:
interface UploadResponse {
success: boolean;
url: string;
filename: string;
size: number;
contentType: string;
uploadedAt: string;
downloadUrl: string;
}
Example Request (JavaScript/FormData):
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('type', 'entity');
formData.append('entityId', 'entity_123');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
Example Request (cURL):
curl -X POST "http://localhost:3000/api/upload" \
-H "Cookie: next-auth.session-token=<token>" \
-F "file=@/path/to/image.jpg" \
-F "type=entity" \
-F "entityId=entity_123"
Example Response:
{
"success": true,
"url": "https://blob.vercel-storage.com/abc123-image.jpg",
"downloadUrl": "https://blob.vercel-storage.com/abc123-image.jpg?download=1",
"filename": "company-logo.jpg",
"size": 245760,
"contentType": "image/jpeg",
"uploadedAt": "2025-01-15T14:30:00Z"
}
Entity File Uploadβ
Upload files specifically for entity profiles (logos, images, documents).
Endpoint: POST /api/entities/upload
Content Type: multipart/form-data
Request Body:
file
(File, required): The file to uploadentityId
(string, required): The entity ID to associate the file withfileType
(string, optional): File purpose ('logo', 'image', 'document')
Response:
interface EntityUploadResponse {
success: boolean;
url: string;
filename: string;
size: number;
contentType: string;
entityId: string;
fileType: string;
uploadedAt: string;
}
Example Request:
const formData = new FormData();
formData.append('file', logoFile);
formData.append('entityId', 'entity_456');
formData.append('fileType', 'logo');
const response = await fetch('/api/entities/upload', {
method: 'POST',
body: formData
});
Example Response:
{
"success": true,
"url": "https://blob.vercel-storage.com/def456-logo.png",
"filename": "company-logo.png",
"size": 156420,
"contentType": "image/png",
"entityId": "entity_456",
"fileType": "logo",
"uploadedAt": "2024-01-15T14:30:00Z"
}
Opportunity File Uploadβ
Upload files for opportunity listings (attachments, requirements, media).
Endpoint: POST /api/opportunities/upload
Content Type: multipart/form-data
Request Body:
file
(File, required): The file to uploadopportunityId
(string, required): The opportunity ID to associate the file withfileType
(string, optional): File purpose ('attachment', 'requirement', 'media')
Response:
interface OpportunityUploadResponse {
success: boolean;
url: string;
filename: string;
size: number;
contentType: string;
opportunityId: string;
fileType: string;
uploadedAt: string;
}
File Type Restrictionsβ
Allowed Image Typesβ
image/jpeg
(.jpg, .jpeg)image/png
(.png)image/gif
(.gif)image/webp
(.webp)image/svg+xml
(.svg)
Allowed Document Typesβ
application/pdf
(.pdf)application/msword
(.doc)application/vnd.openxmlformats-officedocument.wordprocessingml.document
(.docx)text/plain
(.txt)text/markdown
(.md)
Allowed Media Typesβ
video/mp4
(.mp4)video/webm
(.webm)audio/mpeg
(.mp3)audio/wav
(.wav)
File Size Limitsβ
- Images: Maximum 5MB per file
- Documents: Maximum 10MB per file
- Media: Maximum 50MB per file
- General uploads: Maximum 25MB per file
Security Featuresβ
File Validationβ
- MIME type checking: Server-side validation of actual file content
- File signature verification: Magic number validation for security
- Filename sanitization: Automatic cleaning of malicious filenames
- Size validation: Strict enforcement of size limits
Access Controlβ
- User authentication: All uploads require valid session
- Entity ownership: Users can only upload to entities they own
- Opportunity permissions: Upload permissions based on opportunity access
- Admin override: Administrators can upload to any entity/opportunity
Storage Securityβ
- Secure URLs: Time-limited signed URLs for downloads
- Encrypted storage: Files encrypted at rest in Vercel Blob
- Access logging: All upload and download activities logged
- Virus scanning: Automatic malware detection (production only)
Error Handlingβ
HTTP Status Codesβ
200 OK
- Upload successful400 Bad Request
- Invalid file or parameters401 Unauthorized
- Authentication required403 Forbidden
- Insufficient permissions413 Payload Too Large
- File exceeds size limit415 Unsupported Media Type
- File type not allowed500 Internal Server Error
- Server error
Error Response Formatβ
{
"success": false,
"error": "Error message describing what went wrong",
"code": "ERROR_CODE",
"details": {
"maxSize": "5MB",
"allowedTypes": ["image/jpeg", "image/png"],
"actualSize": "7.2MB",
"actualType": "image/bmp"
}
}
Common Error Codesβ
FILE_TOO_LARGE
- File exceeds maximum size limitINVALID_FILE_TYPE
- File type not allowedINVALID_FILE_CONTENT
- File content doesn't match extensionMISSING_PERMISSIONS
- User lacks upload permissionsENTITY_NOT_FOUND
- Referenced entity doesn't existOPPORTUNITY_NOT_FOUND
- Referenced opportunity doesn't existUPLOAD_FAILED
- Storage service error
Rate Limitingβ
- General uploads: 100 uploads per hour per user
- Entity uploads: 50 uploads per hour per entity
- Opportunity uploads: 50 uploads per hour per opportunity
- Large files (>10MB): 10 uploads per hour per user
Integration Examplesβ
React File Upload Componentβ
import React, { useState } from 'react';
import { useSession } from 'next-auth/react';
interface FileUploadProps {
entityId?: string;
opportunityId?: string;
onUploadComplete?: (result: UploadResponse) => void;
maxSize?: number;
allowedTypes?: string[];
}
export function FileUpload({
entityId,
opportunityId,
onUploadComplete,
maxSize = 5 * 1024 * 1024, // 5MB default
allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
}: FileUploadProps) {
const { data: session } = useSession();
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file || !session) return;
// Client-side validation
if (file.size > maxSize) {
setError(`File size must be less than ${Math.round(maxSize / 1024 / 1024)}MB`);
return;
}
if (!allowedTypes.includes(file.type)) {
setError(`File type ${file.type} is not allowed`);
return;
}
setError(null);
setUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
if (entityId) {
formData.append('type', 'entity');
formData.append('entityId', entityId);
} else if (opportunityId) {
formData.append('type', 'opportunity');
formData.append('opportunityId', opportunityId);
}
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Upload failed');
}
const result = await response.json();
onUploadComplete?.(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload failed');
} finally {
setUploading(false);
}
};
return (
<div className="file-upload">
<input
type="file"
onChange={handleFileSelect}
disabled={uploading || !session}
accept={allowedTypes.join(',')}
className="file-input"
/>
{uploading && (
<div className="upload-progress">
<span>Uploading...</span>
</div>
)}
{error && (
<div className="upload-error">
<span>{error}</span>
</div>
)}
</div>
);
}
Upload with Progress Trackingβ
async function uploadWithProgress(
file: File,
options: { entityId?: string; opportunityId?: string },
onProgress?: (progress: number) => void
) {
return new Promise<UploadResponse>((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);
if (options.entityId) {
formData.append('type', 'entity');
formData.append('entityId', options.entityId);
} else if (options.opportunityId) {
formData.append('type', 'opportunity');
formData.append('opportunityId', options.opportunityId);
}
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = (event.loaded / event.total) * 100;
onProgress?.(progress);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const result = JSON.parse(xhr.responseText);
resolve(result);
} else {
const error = JSON.parse(xhr.responseText);
reject(new Error(error.error || 'Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error during upload'));
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
// Usage example
const handleUpload = async (file: File) => {
try {
const result = await uploadWithProgress(
file,
{ entityId: 'entity_123' },
(progress) => {
console.log(`Upload progress: ${progress.toFixed(1)}%`);
}
);
console.log('Upload complete:', result.url);
} catch (error) {
console.error('Upload failed:', error.message);
}
};
Best Practicesβ
- Client-side Validation: Always validate files before uploading
- Progress Indication: Show upload progress for better UX
- Error Handling: Provide clear error messages to users
- File Optimization: Compress images and optimize files before upload
- Chunked Uploads: Consider chunked uploads for large files
- Retry Logic: Implement retry mechanisms for failed uploads
- Preview Generation: Generate thumbnails for images
- Cleanup: Remove temporary files and handle upload cancellation
Testingβ
Use browser developer tools or tools like Postman to test upload endpoints:
# Test with cURL
curl -X POST "http://localhost:3000/api/upload" \
-H "Cookie: next-auth.session-token=your-token" \
-F "file=@test-image.jpg" \
-F "type=entity" \
-F "entityId=test-entity-123"
Monitoring and Analyticsβ
Upload statistics and metrics can be tracked through:
- File size and type distribution
- Upload success/failure rates
- User upload patterns
- Storage usage monitoring
- Performance metrics (upload speed, time to complete)