ghostguild-org/server/api/upload/image.post.js
Jennie Robinson Faber b7279f57d1 Add Zod validation, fix mass assignment, remove test endpoints and dead code
- Add centralized Zod schemas (server/utils/schemas.js) and validateBody
  utility for all API endpoints
- Fix critical mass assignment in member creation: raw body no longer
  passed to new Member(), only validated fields (email, name, circle,
  contributionTier) are accepted
- Apply Zod validation to login, profile patch, event registration,
  updates, verify-payment, and admin event creation endpoints
- Fix logout cookie flags to match login (httpOnly: true, secure
  conditional on NODE_ENV)
- Delete unauthenticated test/debug endpoints (test-connection,
  test-subscription, test-bot)
- Remove sensitive console.log statements from Helcim and member
  endpoints
- Remove unused bcryptjs dependency
- Add 10MB file size limit on image uploads
- Use runtime config for JWT secret across all endpoints
- Add 38 validation tests (117 total, all passing)
2026-03-01 14:02:46 +00:00

83 lines
No EOL
2.3 KiB
JavaScript

import { v2 as cloudinary } from 'cloudinary'
import { requireAuth } from '../../utils/auth.js'
// Configure Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
})
export default defineEventHandler(async (event) => {
try {
await requireAuth(event)
// Parse the multipart form data
const formData = await readMultipartFormData(event)
if (!formData || formData.length === 0) {
throw createError({
statusCode: 400,
statusMessage: 'No file provided'
})
}
// Find the file in the form data
const fileData = formData.find(item => item.name === 'file')
if (!fileData) {
throw createError({
statusCode: 400,
statusMessage: 'No file found in upload'
})
}
// Validate file type
if (!fileData.type?.startsWith('image/')) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid file type. Only images are allowed.'
})
}
// Validate file size (10MB limit)
const maxSize = 10 * 1024 * 1024
if (fileData.data.length > maxSize) {
throw createError({
statusCode: 400,
statusMessage: 'File too large. Maximum size is 10MB.'
})
}
// Convert buffer to base64 for Cloudinary upload
const base64File = `data:${fileData.type};base64,${fileData.data.toString('base64')}`
// Upload to Cloudinary
const result = await cloudinary.uploader.upload(base64File, {
folder: 'ghost-guild/events',
transformation: [
{ quality: 'auto', fetch_format: 'auto' },
{ width: 1200, height: 630, crop: 'fill' } // Standard social media dimensions
],
allowed_formats: ['jpg', 'png', 'webp', 'gif'],
resource_type: 'image'
})
return {
success: true,
secure_url: result.secure_url,
public_id: result.public_id,
width: result.width,
height: result.height,
format: result.format,
bytes: result.bytes
}
} catch (error) {
console.error('Image upload error:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || 'Image upload failed'
})
}
})