diff --git a/.env b/.env index c8fcdc9..ac67ec6 100644 --- a/.env +++ b/.env @@ -10,7 +10,7 @@ MYSQL_PASSWORD=dev_password_123 MYSQL_ROOT_PASSWORD=dev_root_password_123 # CORS Configuration for remote access -CORS_ORIGIN=* +CORS_ORIGIN=http://esprimo:3000,http://localhost:3000,http://localhost:5173 # Development URLs: # - Frontend: http://192.168.178.94:3000 @@ -27,3 +27,4 @@ DOCKER_REGISTRY=docker.citysensor.de # - Use this for testing Linux-specific behavior # - Access from Mac via: http://192.168.178.94:3000 # - SSH tunnel for secure access: ssh -L 3000:192.168.178.94:3000 user@server +ALLOW_INSECURE_CORS=0 diff --git a/NODEJS_README.md b/NODEJS_README.md index b00cdde..ae2cf69 100644 --- a/NODEJS_README.md +++ b/NODEJS_README.md @@ -133,6 +133,7 @@ npm run dev - `GET /api/images/recipe/:recipeId` - Get recipe images - `GET /api/images/serve/:imagePath` - Serve image file - `GET /api/images/:id` - Get image metadata +- `POST /api/images/reorder/:recipeId` - Reorder all images for a recipe (body: `{ "order": [imageId1, imageId2, ...] }`) renames files sequentially `_0.*`, `_1.*`, ... ### Health - `GET /api/health` - Server health check diff --git a/README.md b/README.md index 3b1800b..2489819 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ Dieses Repository enthält zwei Welten: den modernen Node.js / React Stack sowie | Service | Profil | Port Host -> Container | Beschreibung | |--------------|-----------|------------------------|--------------| -| mysql | default | 3307 -> 3306 | MySQL 8 Datenbank | -| backend | default | 3001 -> 3001 | Node.js API (Express + Prisma) | -| frontend | default | 3000 -> 80 | React Build via nginx | -| phpmyadmin | admin | 8083 -> 80 | DB Verwaltung | +| mysql | (auto) | 3307 -> 3306 | MySQL 8 Datenbank | +| backend | (auto) | 3001 -> 3001 | Node.js API (Express + Prisma) | +| frontend | (auto) | 3000 -> 80 | React Build via nginx | +| phpmyadmin | admin | 8083 -> 80 | DB Verwaltung (optional) | ## Node / Modern Stack Empfohlene Node Version: **22.12.0** (`.nvmrc`, `.tool-versions`). @@ -59,11 +59,11 @@ docker compose logs -f mysql ``` ## CORS Hinweis -Aktuell ist `CORS_ORIGIN="*"` (Testphase). Für Produktion einschränken, z.B.: -```yaml -CORS_ORIGIN: http://esprimo:3000,http://localhost:3000 +Das Backend akzeptiert nur Ursprünge aus `CORS_ORIGIN` (kommagetrennt). In Produktion wird ein Wildcard `*` geblockt, außer du setzt bewusst `ALLOW_INSECURE_CORS=1` (nicht empfohlen). Beispiel: +```env +CORS_ORIGIN=http://esprimo:3000,http://localhost:3000 ``` -Danach Backend neu bauen. +Nach Änderung Backend neu bauen / starten. ## Datenbankzugang diff --git a/backend/.env b/backend/.env index 9230475..9a94543 100644 --- a/backend/.env +++ b/backend/.env @@ -1,12 +1,13 @@ -# Database -DATABASE_URL="mysql://rezepte_user:rezepte_pass@localhost:3307/rezepte" +# Database (inside container use service name 'mysql') +DATABASE_URL="mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte" # Server PORT=3001 NODE_ENV=development # CORS Configuration -CORS_ORIGIN=* +# Limit default origin; override via compose env if needed +CORS_ORIGIN=http://localhost:3000 # Prisma # DATABASE_URL="file:./dev.db" \ No newline at end of file diff --git a/backend/dist/app.d.ts.map b/backend/dist/app.d.ts.map index 11a16fe..ea878b1 100644 --- a/backend/dist/app.d.ts.map +++ b/backend/dist/app.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA0HtB,eAAe,GAAG,CAAC"} \ No newline at end of file +{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AAoJtB,eAAe,GAAG,CAAC"} \ No newline at end of file diff --git a/backend/dist/app.js b/backend/dist/app.js index 9eb395a..8d6f37e 100644 --- a/backend/dist/app.js +++ b/backend/dist/app.js @@ -27,28 +27,49 @@ const limiter = (0, express_rate_limit_1.default)({ message: 'Too many requests from this IP, please try again later.', }); app.use(limiter); -const allowedOrigins = [ - 'http://localhost:5173', - 'http://localhost:3000', - config_1.config.cors.origin -].filter(Boolean); -const corsConfig = config_1.config.cors.origin === '*' - ? { - origin: true, - credentials: true, - } - : { - origin: allowedOrigins, - credentials: true, - }; -app.use((0, cors_1.default)(corsConfig)); +const insecureOverride = process.env.ALLOW_INSECURE_CORS === '1'; +const isProd = process.env.NODE_ENV === 'production'; +let allowedOrigins = []; +if (config_1.config.cors.origin.includes(',')) { + allowedOrigins = config_1.config.cors.origin.split(',').map(o => o.trim()).filter(Boolean); +} +else if (config_1.config.cors.origin === '*' && (!isProd || insecureOverride)) { + allowedOrigins = ['*']; +} +else { + allowedOrigins = [config_1.config.cors.origin]; +} +allowedOrigins = Array.from(new Set(allowedOrigins.map(o => o.replace(/\/$/, '')))); +if (!isProd && !allowedOrigins.includes('*')) { + ['http://localhost:5173', 'http://localhost:3000'].forEach(def => { + if (!allowedOrigins.includes(def)) + allowedOrigins.push(def); + }); +} +if (isProd && allowedOrigins.includes('*') && !insecureOverride) { + console.warn('[CORS] Wildcard removed in production. Set CORS_ORIGIN explicitly or ALLOW_INSECURE_CORS=1 (NOT RECOMMENDED).'); + allowedOrigins = allowedOrigins.filter(o => o !== '*'); +} +app.use((0, cors_1.default)({ + origin: (origin, callback) => { + if (!origin) + return callback(null, true); + if (allowedOrigins.includes('*') || allowedOrigins.includes(origin.replace(/\/$/, ''))) { + return callback(null, true); + } + console.warn(`[CORS] Blocked origin: ${origin}`); + return callback(new Error('CORS not allowed for this origin')); + }, + credentials: true, +})); app.use((req, res, next) => { const origin = req.headers.origin; - if (config_1.config.cors.origin === '*') { + const normalized = origin?.replace(/\/$/, ''); + if (allowedOrigins.includes('*')) { res.header('Access-Control-Allow-Origin', origin || '*'); } - else if (origin && allowedOrigins.includes(origin)) { - res.header('Access-Control-Allow-Origin', origin); + else if (normalized && allowedOrigins.includes(normalized)) { + res.header('Access-Control-Allow-Origin', normalized); } res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); @@ -80,8 +101,12 @@ app.get('/serve/*', (req, res, next) => { resolvedPath: fullPath }); } + const requestOrigin = req.headers.origin; + const chosenOrigin = allowedOrigins.includes('*') + ? (requestOrigin || '*') + : (requestOrigin && allowedOrigins.includes(requestOrigin) ? requestOrigin : allowedOrigins[0] || 'http://localhost:3000'); res.set({ - 'Access-Control-Allow-Origin': 'http://localhost:5173', + 'Access-Control-Allow-Origin': chosenOrigin, 'Access-Control-Allow-Credentials': 'true', 'Cache-Control': 'public, max-age=31536000', }); diff --git a/backend/dist/app.js.map b/backend/dist/app.js.map index 9915f5c..0071ebb 100644 --- a/backend/dist/app.js.map +++ b/backend/dist/app.js.map @@ -1 +1 @@ -{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/routes/images.d.ts.map b/backend/dist/routes/images.d.ts.map index fef4eba..34dc490 100644 --- a/backend/dist/routes/images.d.ts.map +++ b/backend/dist/routes/images.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0RxB,eAAe,MAAM,CAAC"} \ No newline at end of file +{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiYxB,eAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/backend/dist/routes/images.js b/backend/dist/routes/images.js index 8d31ee1..2a7b31d 100644 --- a/backend/dist/routes/images.js +++ b/backend/dist/routes/images.js @@ -1,11 +1,44 @@ "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 = __importDefault(require("multer")); +const multer_1 = __importStar(require("multer")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const config_1 = require("../config/config"); @@ -19,29 +52,68 @@ const getUploadsDir = (subPath) => { : 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.body.recipeNumber || req.params.recipeNumber; + const recipeNumber = req.recipeNumber || req.body.recipeNumber || req.params.recipeNumber; if (!recipeNumber) { return cb(new Error('Recipe number is required'), ''); } const uploadDir = getUploadsDir(recipeNumber); - if (!fs_1.default.existsSync(uploadDir)) { - fs_1.default.mkdirSync(uploadDir, { recursive: true }); + 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.body.recipeNumber || req.params.recipeNumber; + 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.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`))) + ? fs_1.default.readdirSync(uploadDir).filter(f => f.startsWith(`${recipeNumber}_`)) : []; - const nextIndex = existingFiles.length; - const filename = `${recipeNumber}_${nextIndex}.jpg`; + 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); } }); @@ -60,41 +132,31 @@ const upload = (0, multer_1.default)({ } }, }); -router.post('/upload/:recipeId', upload.array('images', 10), async (req, res, next) => { +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; - if (!recipeId) { - return res.status(400).json({ - success: false, - message: 'Recipe ID is required', - }); - } + const recipe = res.locals.recipe; if (!files || files.length === 0) { - return res.status(400).json({ - success: false, - message: 'No files uploaded', - }); + return res.status(400).json({ success: false, message: 'No files uploaded' }); } - const recipe = await prisma.recipe.findUnique({ - where: { id: parseInt(recipeId) } - }); - if (!recipe) { - return res.status(404).json({ - success: false, - message: 'Recipe not found', - }); - } - const imagePromises = files.map(file => { + const images = await Promise.all(files.map(file => { const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`; return prisma.recipeImage.create({ - data: { - recipeId: parseInt(recipeId), - filePath: relativePath, - } + data: { recipeId: Number(recipeId), filePath: relativePath } }); - }); - const images = await Promise.all(imagePromises); + })); return res.status(201).json({ success: true, data: images, @@ -104,11 +166,8 @@ router.post('/upload/:recipeId', upload.array('images', 10), async (req, res, ne 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); - } - }); + files.forEach(file => { if (fs_1.default.existsSync(file.path)) + fs_1.default.unlinkSync(file.path); }); } next(error); } @@ -234,5 +293,71 @@ router.get('/:id', async (req, res, next) => { 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 \ No newline at end of file diff --git a/backend/dist/routes/images.js.map b/backend/dist/routes/images.js.map index 78039e1..c128059 100644 --- a/backend/dist/routes/images.js.map +++ b/backend/dist/routes/images.js.map @@ -1 +1 @@ -{"version":3,"file":"images.js","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"images.js","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/src/routes/images.ts b/backend/src/routes/images.ts index 39fa077..52539e5 100644 --- a/backend/src/routes/images.ts +++ b/backend/src/routes/images.ts @@ -1,6 +1,6 @@ import { Router, Request, Response, NextFunction } from 'express'; import { PrismaClient } from '@prisma/client'; -import multer from 'multer'; +import multer, { MulterError } from 'multer'; import path from 'path'; import fs from 'fs'; import { config } from '../config/config'; @@ -21,38 +21,72 @@ const getUploadsDir = (subPath?: string): string => { return subPath ? path.join(baseDir, subPath) : baseDir; }; -// Configure multer for file uploads +// Middleware: load recipe by :recipeId and attach recipeNumber for storage +const loadRecipe = async (req: Request, res: Response, next: NextFunction) => { + 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' }); + } + // Attach for later usage + (req as any).recipeNumber = recipe.recipeNumber; + res.locals.recipe = recipe; + next(); + } catch (err) { + next(err); + } +}; + +// Map mimetypes to extensions +const mimeExt: Record = { + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp' +}; + +// Configure multer for file uploads (uses injected recipeNumber) const storage = multer.diskStorage({ destination: (req, file, cb) => { - const recipeNumber = req.body.recipeNumber || req.params.recipeNumber; + const recipeNumber = (req as any).recipeNumber || req.body.recipeNumber || req.params.recipeNumber; if (!recipeNumber) { return cb(new Error('Recipe number is required'), ''); } - const uploadDir = getUploadsDir(recipeNumber); - - // Create directory if it doesn't exist - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); + try { + if (!fs.existsSync(uploadDir)) { + fs.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.body.recipeNumber || req.params.recipeNumber; + const recipeNumber = (req as any).recipeNumber || req.body.recipeNumber || req.params.recipeNumber; if (!recipeNumber) { return cb(new Error('Recipe number is required'), ''); } - - // Get existing files count to determine next index const uploadDir = getUploadsDir(recipeNumber); - const existingFiles = fs.existsSync(uploadDir) - ? fs.readdirSync(uploadDir).filter(f => f.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`))) + const existingFiles = fs.existsSync(uploadDir) + ? fs.readdirSync(uploadDir).filter(f => f.startsWith(`${recipeNumber}_`)) : []; - - const nextIndex = existingFiles.length; - const filename = `${recipeNumber}_${nextIndex}.jpg`; - + // Determine next index by scanning existing indices + 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); } }); @@ -72,65 +106,44 @@ const upload = multer({ }, }); -// Upload images for a recipe -router.post('/upload/:recipeId', upload.array('images', 10), async (req: Request, res: Response, next: NextFunction) => { +// Upload images for a recipe (preload recipe middleware before multer) +router.post('/upload/:recipeId', loadRecipe, (req, res, next) => { + const mw = upload.array('images', 10); + mw(req, res, function(err: any) { + if (err) { + if (err instanceof 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: Request, res: Response, next: NextFunction) => { try { const { recipeId } = req.params; const files = req.files as Express.Multer.File[]; - - if (!recipeId) { - return res.status(400).json({ - success: false, - message: 'Recipe ID is required', - }); - } - + const recipe = res.locals.recipe; + if (!files || files.length === 0) { - return res.status(400).json({ - success: false, - message: 'No files uploaded', - }); + return res.status(400).json({ success: false, message: 'No files uploaded' }); } - - // Get recipe to validate it exists and get recipe number - const recipe = await prisma.recipe.findUnique({ - where: { id: parseInt(recipeId) } - }); - - if (!recipe) { - return res.status(404).json({ - success: false, - message: 'Recipe not found', - }); - } - - // Create database entries for uploaded images - const imagePromises = files.map(file => { + + const images = await Promise.all(files.map(file => { const relativePath = `uploads/${recipe.recipeNumber}/${file.filename}`; return prisma.recipeImage.create({ - data: { - recipeId: parseInt(recipeId), - filePath: relativePath, - } + data: { recipeId: Number(recipeId), filePath: relativePath } }); - }); - - const images = await Promise.all(imagePromises); - + })); + return res.status(201).json({ success: true, data: images, message: `${files.length} images uploaded successfully`, }); } catch (error) { - // Clean up uploaded files if database operation fails if (req.files) { const files = req.files as Express.Multer.File[]; - files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); - } - }); + files.forEach(file => { if (fs.existsSync(file.path)) fs.unlinkSync(file.path); }); } next(error); } @@ -287,4 +300,94 @@ router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { } }); +// Reorder images for a recipe +router.post('/reorder/:recipeId', async (req: Request, res: Response, next: NextFunction) => { + /* + Payload: { order: number[] } + - order: Array of image IDs in desired final sequence. + - First becomes *_0.ext (main), second *_1.ext, ... + */ + try { + const { recipeId } = req.params; + const { order } = req.body as { order?: number[] }; + + 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' }); + + // Validate order covers exactly all image IDs + 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.existsSync(baseDir)) { + return res.status(500).json({ success: false, message: 'Upload directory missing on server' }); + } + + // Build mapping: targetIndex -> image object + const idToImage = new Map(images.map(i => [i.id, i] as const)); + const newSequence = order.map((id, idx) => ({ idx, image: idToImage.get(id)! })); + + // Phase 1: rename to temporary names to avoid collisions + const tempRenames: { from: string; to: string; final: string; idx: number; ext: string }[] = []; + 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}`; + // Previous implementation incorrectly traversed ../../ resulting in /uploads/... (missing /app prefix in container) + // image.filePath is like 'uploads/R100/R100_0.png' and baseDir points to '/app/uploads/R100' + // So we join baseDir with just the filename to locate the current file. + const oldFull = path.join(baseDir, path.basename(fileName)); + const tempName = `${recipeNumber}__reorder_${idx}_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`; + const tempFull = path.join(baseDir, tempName); + const finalFull = path.join(baseDir, finalName); + if (!fs.existsSync(oldFull)) { + return res.status(500).json({ success: false, message: `File missing on disk: ${fileName}` }); + } + fs.renameSync(oldFull, tempFull); + tempRenames.push({ from: tempFull, to: finalFull, final: `uploads/${recipeNumber}/${finalName}`, idx, ext }); + } + + // Phase 2: rename temp -> final + for (const r of tempRenames) { + fs.renameSync(r.from, r.to); + } + + // Update DB entries in a transaction-like sequence + // (MySQL autocommit; if one fails we attempt best-effort rollback is complex -> assume disk ops succeeded) + 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); + } +}); + export default router; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index bd5de9f..42f0b8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,6 @@ services: image: mysql:8.0 container_name: rezepte_mysql restart: always - profiles: - - default environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-change_this_root_password} MYSQL_DATABASE: ${MYSQL_DATABASE:-rezepte} @@ -45,12 +43,11 @@ services: context: ./backend dockerfile: Dockerfile container_name: rezepte-backend - profiles: - - default environment: NODE_ENV: ${NODE_ENV:-production} PORT: ${BACKEND_PORT:-3001} - DATABASE_URL: ${DATABASE_URL:-mysql://${MYSQL_USER:-rezepte_user}:${MYSQL_PASSWORD:-change_this_password}@mysql:3306/${MYSQL_DATABASE:-rezepte}} + # Explicit DB URL (override to ensure service hostname used inside container) + DATABASE_URL: mysql://${MYSQL_USER:-rezepte_user}:${MYSQL_PASSWORD:-dev_password_123}@mysql:3306/${MYSQL_DATABASE:-rezepte} JWT_SECRET: ${JWT_SECRET:-please_change_to_secure_32_char_min} UPLOAD_PATH: ${UPLOAD_PATH:-/app/uploads} MAX_FILE_SIZE: ${MAX_FILE_SIZE:-5242880} @@ -74,8 +71,6 @@ services: args: VITE_API_URL: http://localhost:3001/api container_name: rezepte-frontend - profiles: - - default ports: - "${FRONTEND_PORT:-3000}:80" networks: diff --git a/frontend/src/components/RecipeDetail.css b/frontend/src/components/RecipeDetail.css index 5ec7e93..e64b493 100644 --- a/frontend/src/components/RecipeDetail.css +++ b/frontend/src/components/RecipeDetail.css @@ -185,7 +185,8 @@ border-radius: 12px; overflow: hidden; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); - background: #f8f9fa; + /* Use pure white so transparent PNGs are not auf grauem Untergrund leicht "ausgewaschen" */ + background: #ffffff; max-height: 400px; } @@ -475,6 +476,8 @@ width: 100%; height: 150px; overflow: hidden; + /* Neutraler Hintergrund für transparente Bereiche */ + background: #ffffff; } .image-preview img { diff --git a/frontend/src/components/RecipeDetail.tsx b/frontend/src/components/RecipeDetail.tsx index 75ee15a..9dcc7bb 100644 --- a/frontend/src/components/RecipeDetail.tsx +++ b/frontend/src/components/RecipeDetail.tsx @@ -102,6 +102,28 @@ const RecipeDetail: React.FC = () => { } }; + const handleReorder = async (imageId: number, direction: 'up' | 'down') => { + if (!recipe || !recipe.images || !id) return; + const idx = recipe.images.findIndex(img => img.id === imageId); + if (idx === -1) return; + const swapWith = direction === 'up' ? idx - 1 : idx + 1; + if (swapWith < 0 || swapWith >= recipe.images.length) return; + + const newOrder = [...recipe.images]; + [newOrder[idx], newOrder[swapWith]] = [newOrder[swapWith], newOrder[idx]]; + + try { + // Send ordered IDs + await imageApi.reorderImages(parseInt(id), newOrder.map(i => i.id)); + // Reload recipe to pick up new filenames (indexes changed) + const response = await recipeApi.getRecipe(parseInt(id)); + if (response.success) setRecipe(response.data); + } catch (e) { + console.error('Reorder failed', e); + setError('Fehler beim Neusortieren der Bilder'); + } + }; + if (loading) { return (
@@ -143,6 +165,15 @@ const RecipeDetail: React.FC = () => { ); } + // Helper utilities for image handling + const getFileName = (fp: string) => fp.split('/').pop() || ''; + const isMainImage = (fileName: string) => /_0\.(jpg|jpeg|png|webp)$/i.test(fileName); + const isStepImage = (fileName: string) => /_[1-9]\d*\.(jpg|jpeg|png|webp)$/i.test(fileName); + const getStepIndex = (fileName: string) => { + const m = fileName.match(/_(\d+)\.(jpg|jpeg|png|webp)$/i); + return m ? parseInt(m[1], 10) : -1; + }; + return (
{/* Header */} @@ -185,12 +216,12 @@ const RecipeDetail: React.FC = () => { {recipe.images && recipe.images.length > 0 && (
{(() => { - // Find the main image (xxx_0.jpg) - const mainImage = recipe.images.find(image => { - const fileName = image.filePath.split('/').pop() || ''; - return fileName.includes('_0.jpg'); - }); - + // Find the main image *_0.; if none, fallback to first image + let mainImage = recipe.images.find(img => isMainImage(getFileName(img.filePath))); + if (!mainImage && recipe.images.length > 0) { + mainImage = recipe.images[0]; + } + if (mainImage) { return ( { src={imageApi.getImageUrl(image.filePath)} alt={`Bild ${index + 1}`} /> +
+ + +
@@ -330,19 +377,11 @@ const RecipeDetail: React.FC = () => { {(() => { // Get all preparation images (exclude main image _0.jpg) const preparationImages = recipe.images - ?.filter(image => { - const fileName = image.filePath.split('/').pop() || ''; - // Match pattern like R005_1.jpg, R005_2.jpg, etc. but not R005_0.jpg - return fileName.match(/_[1-9]\d*\.jpg$/); + ?.filter(img => { + const fn = getFileName(img.filePath); + return isStepImage(fn); }) - .sort((a, b) => { - // Sort by the number in the filename - const getNumber = (path: string) => { - const match = path.match(/_(\d+)\.jpg$/); - return match ? parseInt(match[1]) : 0; - }; - return getNumber(a.filePath) - getNumber(b.filePath); - }) || []; + .sort((a, b) => getStepIndex(getFileName(a.filePath)) - getStepIndex(getFileName(b.filePath))) || []; return recipe.instructions.split('\n').map((instruction, index) => { // Get the corresponding image for this step diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 2a9553e..4ed3b27 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -178,6 +178,12 @@ export const imageApi = { const response = await api.delete(`/images/${imageId}`); return response.data; }, + + // Reorder images for a recipe + reorderImages: async (recipeId: number, orderedIds: number[]): Promise> => { + const response = await api.post(`/images/reorder/${recipeId}`, { order: orderedIds }); + return response.data; + }, }; // Health check diff --git a/uploads/R100/R100_0_container.png b/uploads/R100/R100_0_container.png new file mode 100644 index 0000000..e649a07 Binary files /dev/null and b/uploads/R100/R100_0_container.png differ