const { Router } = require('express'); const multer = require('multer'); const sharp = require('sharp'); const { v4: uuidv4 } = require('uuid'); const https = require('https'); const http = require('http'); const { URL } = require('url'); const { authMiddleware } = require('../auth'); const { getBoard, getCollectionMember, createImage } = require('../db'); const { putBuffer, getImageUrl, MIME_TO_EXT, MAX_FILE_SIZE } = require('../minio'); const { probeVideo, extractPoster } = require('../video-utils'); const router = Router(); const IMAGE_MIME_TYPES = [ 'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml', ]; const VIDEO_MIME_TYPES = [ 'video/mp4', 'video/webm', 'video/quicktime', ]; const ALLOWED_MIME_TYPES = [...IMAGE_MIME_TYPES, ...VIDEO_MIME_TYPES]; const MAX_FILE_SIZE_LABEL = `${MAX_FILE_SIZE / 1024 / 1024}MB`; // Multer config: memory storage, configurable size limit const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_FILE_SIZE }, fileFilter: (_req, file, cb) => { if (ALLOWED_MIME_TYPES.includes(file.mimetype)) { cb(null, true); } else { cb(new Error(`Unsupported file type: ${file.mimetype}`)); } }, }); // All routes require auth router.use(authMiddleware); /** * Helper: check board access for editor+ */ function checkEditorAccess(req, res) { const board = getBoard(req.params.boardId); if (!board) { res.status(404).json({ error: 'Board not found' }); return null; } const member = getCollectionMember(board.collection_id, req.user.id); const hierarchy = { owner: 3, editor: 2, viewer: 1 }; if (!member || (hierarchy[member.role] || 0) < 2) { res.status(403).json({ error: 'Editor or owner access required' }); return null; } return board; } /** * Get image dimensions using sharp. Returns { width, height } or null for SVG. */ async function getImageDimensions(buffer, mimeType) { if (mimeType === 'image/svg+xml') { return { width: null, height: null }; } try { const metadata = await sharp(buffer).metadata(); return { width: metadata.width || null, height: metadata.height || null }; } catch { return { width: null, height: null }; } } /** * Detect whether a MIME type is image or video. */ function classifyMedia(mimeType) { if (VIDEO_MIME_TYPES.includes(mimeType)) return 'video'; return 'image'; } /** * Upload a media file (image or video) to MinIO. * For videos: extracts poster frame + metadata via ffmpeg at upload time * so the board never needs a