"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const client_1 = require("@prisma/client"); const multer_1 = __importStar(require("multer")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const config_1 = require("../config/config"); const router = (0, express_1.Router)(); const prisma = new client_1.PrismaClient(); const getUploadsDir = (subPath) => { const localUploadsDir = path_1.default.join(process.cwd(), 'uploads'); const legacyUploadsDir = path_1.default.join(process.cwd(), '../../uploads'); const baseDir = fs_1.default.existsSync(localUploadsDir) ? localUploadsDir : legacyUploadsDir; return subPath ? path_1.default.join(baseDir, subPath) : baseDir; }; const loadRecipe = async (req, res, next) => { try { const { recipeId } = req.params; if (!recipeId || isNaN(Number(recipeId))) { return res.status(400).json({ success: false, message: 'Valid recipeId is required' }); } const recipe = await prisma.recipe.findUnique({ where: { id: Number(recipeId) } }); if (!recipe) { return res.status(404).json({ success: false, message: 'Recipe not found' }); } req.recipeNumber = recipe.recipeNumber; res.locals.recipe = recipe; next(); } catch (err) { next(err); } }; const mimeExt = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }; const storage = multer_1.default.diskStorage({ destination: (req, file, cb) => { const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber; if (!recipeNumber) { return cb(new Error('Recipe number is required'), ''); } const uploadDir = getUploadsDir(recipeNumber); try { if (!fs_1.default.existsSync(uploadDir)) { fs_1.default.mkdirSync(uploadDir, { recursive: true }); } } catch (e) { return cb(new Error('Failed to prepare upload directory'), ''); } cb(null, uploadDir); }, filename: (req, file, cb) => { const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber; if (!recipeNumber) { return cb(new Error('Recipe number is required'), ''); } const uploadDir = getUploadsDir(recipeNumber); const existingFiles = fs_1.default.existsSync(uploadDir) ? fs_1.default.readdirSync(uploadDir).filter(f => f.startsWith(`${recipeNumber}_`)) : []; let maxIndex = -1; existingFiles.forEach(f => { const m = f.match(new RegExp(`^${recipeNumber}_(\\d+)`)); if (m && m[1] !== undefined) { const idx = parseInt(m[1], 10); if (!Number.isNaN(idx) && idx > maxIndex) maxIndex = idx; } }); const nextIndex = maxIndex + 1; const ext = mimeExt[file.mimetype] || 'jpg'; const filename = `${recipeNumber}_${nextIndex}.${ext}`; cb(null, filename); } }); const upload = (0, multer_1.default)({ storage, limits: { fileSize: config_1.config.upload.maxFileSize, }, fileFilter: (req, file, cb) => { const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Invalid file type. Only JPEG, PNG and WebP are allowed.')); } }, }); router.post('/upload/:recipeId', loadRecipe, (req, res, next) => { const mw = upload.array('images', 10); mw(req, res, function (err) { if (err) { if (err instanceof multer_1.MulterError) { return res.status(400).json({ success: false, message: `Upload error: ${err.message}` }); } return res.status(400).json({ success: false, message: err.message || 'Upload failed' }); } next(); }); }, async (req, res, next) => { try { const { recipeId } = req.params; const files = req.files; const recipe = res.locals.recipe; if (!files || files.length === 0) { return res.status(400).json({ success: false, message: 'No files uploaded' }); } const images = await Promise.all(files.map(file => { const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`; return prisma.recipeImage.create({ data: { recipeId: Number(recipeId), filePath: relativePath } }); })); return res.status(201).json({ success: true, data: images, message: `${files.length} images uploaded successfully`, }); } catch (error) { if (req.files) { const files = req.files; files.forEach(file => { if (fs_1.default.existsSync(file.path)) fs_1.default.unlinkSync(file.path); }); } next(error); } }); router.delete('/:id', async (req, res, next) => { try { const { id } = req.params; if (!id) { return res.status(400).json({ success: false, message: 'Image ID is required', }); } const image = await prisma.recipeImage.findUnique({ where: { id: parseInt(id) } }); if (!image) { return res.status(404).json({ success: false, message: 'Image not found', }); } const fullPath = path_1.default.join(process.cwd(), '../..', image.filePath); if (fs_1.default.existsSync(fullPath)) { fs_1.default.unlinkSync(fullPath); } await prisma.recipeImage.delete({ where: { id: parseInt(id) } }); return res.json({ success: true, message: 'Image deleted successfully', }); } catch (error) { next(error); } }); router.get('/recipe/:recipeId', async (req, res, next) => { try { const { recipeId } = req.params; if (!recipeId) { return res.status(400).json({ success: false, message: 'Recipe ID is required', }); } const images = await prisma.recipeImage.findMany({ where: { recipeId: parseInt(recipeId) }, orderBy: { id: 'asc' } }); return res.json({ success: true, data: images, }); } catch (error) { next(error); } }); router.get('/serve/:imagePath(*)', (req, res, next) => { try { const imagePath = req.params.imagePath; if (!imagePath) { return res.status(400).json({ success: false, message: 'Image path is required', }); } const cleanPath = imagePath.replace(/^uploads\//, ''); const fullPath = path_1.default.join(getUploadsDir(), cleanPath); console.log(`Serving image: ${imagePath} -> ${fullPath}`); if (!fs_1.default.existsSync(fullPath)) { console.log(`Image not found: ${fullPath}`); return res.status(404).json({ success: false, message: 'Image not found', requestedPath: imagePath, resolvedPath: fullPath }); } const allowedOrigins = ['http://localhost:5173', 'http://localhost:3000']; const origin = req.headers.origin; const corsOrigin = process.env.CORS_ORIGIN === '*' ? (origin || '*') : (origin && allowedOrigins.includes(origin)) ? origin : 'http://localhost:3000'; res.set({ 'Access-Control-Allow-Origin': corsOrigin, 'Access-Control-Allow-Credentials': 'true', 'Cache-Control': 'public, max-age=31536000', }); return res.sendFile(path_1.default.resolve(fullPath)); } catch (error) { console.error('Error serving image:', error); next(error); } }); router.get('/:id', async (req, res, next) => { try { const { id } = req.params; if (!id) { return res.status(400).json({ success: false, message: 'Image ID is required', }); } const image = await prisma.recipeImage.findUnique({ where: { id: parseInt(id) } }); if (!image) { return res.status(404).json({ success: false, message: 'Image not found', }); } return res.json({ success: true, data: image, }); } catch (error) { next(error); } }); router.post('/reorder/:recipeId', async (req, res, next) => { try { const { recipeId } = req.params; const { order } = req.body; if (!recipeId || isNaN(Number(recipeId))) { return res.status(400).json({ success: false, message: 'Valid recipeId required' }); } if (!Array.isArray(order) || order.length === 0) { return res.status(400).json({ success: false, message: 'order (non-empty array) required' }); } const rid = Number(recipeId); const recipe = await prisma.recipe.findUnique({ where: { id: rid } }); if (!recipe) return res.status(404).json({ success: false, message: 'Recipe not found' }); const images = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } }); if (images.length === 0) return res.status(400).json({ success: false, message: 'No images to reorder' }); const existingIds = images.map(i => i.id).sort((a, b) => a - b); const provided = [...order].sort((a, b) => a - b); if (existingIds.length !== provided.length || !existingIds.every((v, i) => v === provided[i])) { return res.status(400).json({ success: false, message: 'order must contain all image IDs exactly once' }); } const recipeNumber = recipe.recipeNumber; const baseDir = getUploadsDir(recipeNumber); if (!fs_1.default.existsSync(baseDir)) { return res.status(500).json({ success: false, message: 'Upload directory missing on server' }); } const idToImage = new Map(images.map(i => [i.id, i])); const newSequence = order.map((id, idx) => ({ idx, image: idToImage.get(id) })); const tempRenames = []; for (const { idx, image } of newSequence) { const fileName = image.filePath.split('/').pop() || ''; const extMatch = fileName.match(/\.(jpg|jpeg|png|webp)$/i); const ext = extMatch && extMatch[1] ? extMatch[1].toLowerCase() : 'jpg'; const finalName = `${recipeNumber}_${idx}.${ext === 'jpeg' ? 'jpg' : ext}`; const oldFull = path_1.default.join(baseDir, path_1.default.basename(fileName)); const tempName = `${recipeNumber}__reorder_${idx}_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`; const tempFull = path_1.default.join(baseDir, tempName); const finalFull = path_1.default.join(baseDir, finalName); if (!fs_1.default.existsSync(oldFull)) { return res.status(500).json({ success: false, message: `File missing on disk: ${fileName}` }); } fs_1.default.renameSync(oldFull, tempFull); tempRenames.push({ from: tempFull, to: finalFull, final: `uploads/${recipeNumber}/${finalName}`, idx, ext }); } for (const r of tempRenames) { fs_1.default.renameSync(r.from, r.to); } await Promise.all(newSequence.map(({ idx, image }) => { const renameRecord = tempRenames.find(tr => tr.idx === idx); return prisma.recipeImage.update({ where: { id: image.id }, data: { filePath: renameRecord.final } }); })); const updated = await prisma.recipeImage.findMany({ where: { recipeId: rid }, orderBy: { id: 'asc' } }); return res.json({ success: true, message: 'Images reordered', data: updated, }); } catch (error) { next(error); } }); exports.default = router; //# sourceMappingURL=images.js.map