Skip to main content

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 upload
  • type (string, optional): Upload context type ('entity', 'opportunity', 'profile', 'general')
  • entityId (string, optional): Associated entity ID for context
  • opportunityId (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 upload
  • entityId (string, required): The entity ID to associate the file with
  • fileType (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 upload
  • opportunityId (string, required): The opportunity ID to associate the file with
  • fileType (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 successful
  • 400 Bad Request - Invalid file or parameters
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - Insufficient permissions
  • 413 Payload Too Large - File exceeds size limit
  • 415 Unsupported Media Type - File type not allowed
  • 500 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 limit
  • INVALID_FILE_TYPE - File type not allowed
  • INVALID_FILE_CONTENT - File content doesn't match extension
  • MISSING_PERMISSIONS - User lacks upload permissions
  • ENTITY_NOT_FOUND - Referenced entity doesn't exist
  • OPPORTUNITY_NOT_FOUND - Referenced opportunity doesn't exist
  • UPLOAD_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​

  1. Client-side Validation: Always validate files before uploading
  2. Progress Indication: Show upload progress for better UX
  3. Error Handling: Provide clear error messages to users
  4. File Optimization: Compress images and optimize files before upload
  5. Chunked Uploads: Consider chunked uploads for large files
  6. Retry Logic: Implement retry mechanisms for failed uploads
  7. Preview Generation: Generate thumbnails for images
  8. 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)