Input-Text geht, CORS behoben

This commit is contained in:
2025-09-24 21:10:11 +00:00
parent ef4ab9e800
commit a9428fee94
23 changed files with 257 additions and 71 deletions

View File

@@ -23,6 +23,12 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js (22.12.0) for auxiliary scripts
uses: actions/setup-node@v4
with:
node-version: '22.12.0'
check-latest: true
- name: Log in to CitySensor Container Registry - name: Log in to CitySensor Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22.12.0

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
node 22.12.0

View File

@@ -33,6 +33,22 @@ A complete modern web application built with Node.js, TypeScript, React, and MyS
## <20> Quick Start ## <20> Quick Start
### ⚙️ Node Version
Empfohlene Node Version: **22.12.0** (oder LTS 20.19.x). Projekt liefert `.nvmrc`.
Mit nvm aktivieren:
```bash
nvm install 22.12.0
nvm use 22.12.0
```
Automatisch beim Wechsel ins Verzeichnis:
```bash
nvm use
```
Falls ohne nvm: Offizielles Binary oder Paketmanager (Node >=22.12.0) installieren.
### 1. Start the Database (existing Docker setup) ### 1. Start the Database (existing Docker setup)
```bash ```bash
# From main project directory # From main project directory
@@ -42,6 +58,7 @@ docker-compose up -d
### 2. Start the Node.js Backend ### 2. Start the Node.js Backend
```bash ```bash
cd backend cd backend
nvm use # optional, falls nvm
npm install npm install
npm run build npm run build
node dist/app.js node dist/app.js
@@ -50,6 +67,7 @@ node dist/app.js
### 3. Start the React Frontend ### 3. Start the React Frontend
```bash ```bash
cd frontend cd frontend
nvm use # optional
npm install npm install
npm run dev npm run dev
``` ```

115
README.md
View File

@@ -1,28 +1,83 @@
# Rezepte - Docker Setup # Rezepte - Docker & Modern Stack
Eine dockerisierte Version der Rezepte-Verwaltungsanwendung mit PHP, MySQL und phpMyAdmin. Dieses Repository enthält zwei Welten: den modernen Node.js / React Stack sowie die historische PHP Legacy-App. Über Docker Compose Profile kannst du gezielt nur die benötigten Teile starten.
## Komponenten ## Services & Ports
- **PHP-App**: Rezepte-Anwendung (Port 8082) | Service | Profil | Port Host -> Container | Beschreibung |
- **MySQL**: Datenbankserver (Port 3307) |--------------|-----------|------------------------|--------------|
- **phpMyAdmin**: Datenbankadministration (Port 8083) | mysql | default | 3307 -> 3306 | MySQL 8 Datenbank |
| backend | default | 3001 -> 3001 | Node.js API (Express + Prisma) |
| frontend | default | 3000 -> 80 | React Build via nginx |
| php-app | legacy | 8082 -> 80 | Altes PHP Frontend |
| phpmyadmin | admin | 8083 -> 80 | DB Verwaltung |
## Schnellstart ## Node / Modern Stack
Empfohlene Node Version: **22.12.0** (`.nvmrc`, `.tool-versions`).
1. **Docker starten:** Lokal ohne Docker (Entwicklung Hot Reload):
```bash ```bash
docker-compose up -d nvm use
``` cd backend && npm install && npm run dev # startet API auf :3001
cd ../frontend && npm install && npm run dev # startet Vite auf :5173
```
2. **Anwendung öffnen:** ## Docker Nutzung
- Rezepte-App: http://localhost:8082
- phpMyAdmin: http://localhost:8083
3. **Container stoppen:** ### Standard (nur moderner Stack: DB + API + Frontend)
```bash ```bash
docker-compose down docker compose up -d
``` ```
Öffnen: http://localhost:3000
### Mit Legacy PHP zusätzlich
```bash
docker compose --profile legacy up -d
```
Öffnen: http://localhost:8082
### Mit phpMyAdmin zusätzlich
```bash
docker compose --profile admin up -d
```
Öffnen: http://localhost:8083
### Beide Zusatz-Profile
```bash
docker compose --profile legacy --profile admin up -d
```
### Alles stoppen
```bash
docker compose down
```
### Neu bauen (z.B. nach Codeänderungen Backend/Frontend)
```bash
docker compose build backend frontend
docker compose up -d
```
### Nur Backend neu bauen
```bash
docker compose build backend && docker compose up -d backend
```
### Logs
```bash
docker compose logs -f backend
docker compose logs -f frontend
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
```
Danach Backend neu bauen.
## Datenbankzugang
## Datenbankzugang ## Datenbankzugang
@@ -41,23 +96,16 @@ Eine dockerisierte Version der Rezepte-Verwaltungsanwendung mit PHP, MySQL und p
- Username: root - Username: root
- Password: rezepte123 - Password: rezepte123
## Entwicklung ## Entwicklung innerhalb Docker
### Container neu bauen: Status:
```bash ```bash
docker-compose build --no-cache docker compose ps
docker-compose up -d
``` ```
### Logs anzeigen: Direktes DB Login (Host hat mysql Client):
```bash ```bash
docker-compose logs -f php-app mysql -h 127.0.0.1 -P 3307 -u rezepte_user -p'rezepte_pass' rezepte
docker-compose logs -f mysql
```
### Container Status:
```bash
docker-compose ps
``` ```
## Datenvolumes ## Datenvolumes
@@ -66,4 +114,9 @@ Die MySQL-Daten werden in einem Docker-Volume gespeichert und bleiben auch nach
## Datenbankinitialisierung ## Datenbankinitialisierung
Beim ersten Start werden automatisch alle Tabellen und Daten aus den SQL-Dateien importiert. Beim ersten Start importiert MySQL automatisch SQL-Skripte aus `sql-init/`.
## Weiterführend
- Moderne Stack Doku: `NODEJS_README.md`
- Traefik / Registry Deploy: siehe entsprechende `*_SETUP.md` Dateien
- Sicherheitshärtung TODO: CORS einschränken, CSP Header, chown Migration verbessern

View File

@@ -1,5 +1,5 @@
# Backend Dockerfile # Backend Dockerfile
FROM node:18-alpine AS builder FROM node:22.12.0-alpine AS builder
# Install OpenSSL for Prisma compatibility # Install OpenSSL for Prisma compatibility
RUN apk add --no-cache openssl openssl-dev RUN apk add --no-cache openssl openssl-dev
@@ -20,7 +20,7 @@ COPY . .
RUN npm run build RUN npm run build
# Production stage # Production stage
FROM node:18-alpine AS production FROM node:22.12.0-alpine AS production
# Install required system dependencies for Prisma and health checks # Install required system dependencies for Prisma and health checks
RUN apk add --no-cache \ RUN apk add --no-cache \

View File

@@ -1 +1 @@
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA2GtB,eAAe,GAAG,CAAC"} {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,GAAG,6CAAY,CAAC;AA0HtB,eAAe,GAAG,CAAC"}

20
backend/dist/app.js vendored
View File

@@ -32,13 +32,22 @@ const allowedOrigins = [
'http://localhost:3000', 'http://localhost:3000',
config_1.config.cors.origin config_1.config.cors.origin
].filter(Boolean); ].filter(Boolean);
app.use((0, cors_1.default)({ const corsConfig = config_1.config.cors.origin === '*'
origin: allowedOrigins, ? {
credentials: true, origin: true,
})); credentials: true,
}
: {
origin: allowedOrigins,
credentials: true,
};
app.use((0, cors_1.default)(corsConfig));
app.use((req, res, next) => { app.use((req, res, next) => {
const origin = req.headers.origin; const origin = req.headers.origin;
if (origin && allowedOrigins.includes(origin)) { if (config_1.config.cors.origin === '*') {
res.header('Access-Control-Allow-Origin', origin || '*');
}
else if (origin && allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Origin', origin);
} }
res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Credentials', 'true');
@@ -52,6 +61,7 @@ app.use((req, res, next) => {
app.use(express_1.default.json({ limit: '10mb' })); app.use(express_1.default.json({ limit: '10mb' }));
app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' })); app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' }));
app.use(requestLogger_1.requestLogger); app.use(requestLogger_1.requestLogger);
app.use('/uploads', express_1.default.static(path_1.default.join(process.cwd(), 'uploads')));
app.use('/api/health', health_1.default); app.use('/api/health', health_1.default);
app.use('/api/recipes', recipes_1.default); app.use('/api/recipes', recipes_1.default);
app.use('/api/ingredients', ingredients_1.default); app.use('/api/ingredients', ingredients_1.default);

View File

@@ -1 +1 @@
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"} {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}

View File

@@ -1 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":";;;;;;AAAA,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEH,QAAA,MAAM,GAAG;IACpB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI;IAC9B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa;IAE9C,QAAQ,EAAE;QACR,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,gEAAgE;KAClG;IAED,GAAG,EAAE;QACH,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,2BAA2B;QAC7D,SAAS,EAAE,KAAK;KACjB;IAED,MAAM,EAAE;QACN,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,WAAW;QAC5C,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,SAAS,CAAC;QAC7D,YAAY,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,CAAC;KACrE;IAED,IAAI,EAAE;QACJ,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB;KAC3D;CACO,CAAC"} {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":";;;;;;AAAA,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEH,QAAA,MAAM,GAAG;IACpB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI;IAC9B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa;IAE9C,QAAQ,EAAE;QACR,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,0DAA0D;KAC5F;IAED,GAAG,EAAE;QACH,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,2BAA2B;QAC7D,SAAS,EAAE,KAAK;KACjB;IAED,MAAM,EAAE;QACN,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,WAAW;QAC5C,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,SAAS,CAAC;QAC7D,YAAY,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,CAAC;KACrE;IAED,IAAI,EAAE;QACJ,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB;KAC3D;CACO,CAAC"}

View File

@@ -1 +1 @@
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":";;AAAA,qCAAoD;AAEpD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAGxB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IAC9C,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,+BAA+B;QACxC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ;KAClC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAGH,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACtD,IAAI,CAAC;QAEH,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,gCAAgC;YACzC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,4BAA4B;YACrC,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe;SAChE,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"} {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":";;AAAA,qCAAoD;AAEpD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAGxB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IAC9C,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,yBAAyB;QAClC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ;KAClC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAGH,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACtD,IAAI,CAAC;QAEH,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,gCAAgC;YACzC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,4BAA4B;YACrC,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe;SAChE,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View File

@@ -1 +1 @@
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqQxB,eAAe,MAAM,CAAC"} {"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/routes/images.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0RxB,eAAe,MAAM,CAAC"}

View File

@@ -11,13 +11,21 @@ const fs_1 = __importDefault(require("fs"));
const config_1 = require("../config/config"); const config_1 = require("../config/config");
const router = (0, express_1.Router)(); const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient(); 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 storage = multer_1.default.diskStorage({ const storage = multer_1.default.diskStorage({
destination: (req, file, cb) => { destination: (req, file, cb) => {
const recipeNumber = req.body.recipeNumber || req.params.recipeNumber; const recipeNumber = req.body.recipeNumber || req.params.recipeNumber;
if (!recipeNumber) { if (!recipeNumber) {
return cb(new Error('Recipe number is required'), ''); return cb(new Error('Recipe number is required'), '');
} }
const uploadDir = path_1.default.join(process.cwd(), '../../uploads', recipeNumber); const uploadDir = getUploadsDir(recipeNumber);
if (!fs_1.default.existsSync(uploadDir)) { if (!fs_1.default.existsSync(uploadDir)) {
fs_1.default.mkdirSync(uploadDir, { recursive: true }); fs_1.default.mkdirSync(uploadDir, { recursive: true });
} }
@@ -28,7 +36,7 @@ const storage = multer_1.default.diskStorage({
if (!recipeNumber) { if (!recipeNumber) {
return cb(new Error('Recipe number is required'), ''); return cb(new Error('Recipe number is required'), '');
} }
const uploadDir = path_1.default.join(process.cwd(), '../../uploads', recipeNumber); const uploadDir = getUploadsDir(recipeNumber);
const existingFiles = fs_1.default.existsSync(uploadDir) 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.match(new RegExp(`^${recipeNumber}_\\d+\\.jpg$`)))
: []; : [];
@@ -171,7 +179,7 @@ router.get('/serve/:imagePath(*)', (req, res, next) => {
}); });
} }
const cleanPath = imagePath.replace(/^uploads\//, ''); const cleanPath = imagePath.replace(/^uploads\//, '');
const fullPath = path_1.default.join(process.cwd(), '../../uploads', cleanPath); const fullPath = path_1.default.join(getUploadsDir(), cleanPath);
console.log(`Serving image: ${imagePath} -> ${fullPath}`); console.log(`Serving image: ${imagePath} -> ${fullPath}`);
if (!fs_1.default.existsSync(fullPath)) { if (!fs_1.default.existsSync(fullPath)) {
console.log(`Image not found: ${fullPath}`); console.log(`Image not found: ${fullPath}`);
@@ -182,8 +190,13 @@ router.get('/serve/:imagePath(*)', (req, res, next) => {
resolvedPath: fullPath 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({ res.set({
'Access-Control-Allow-Origin': 'http://localhost:5173', 'Access-Control-Allow-Origin': corsOrigin,
'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Credentials': 'true',
'Cache-Control': 'public, max-age=31536000', 'Cache-Control': 'public, max-age=31536000',
}); });

File diff suppressed because one or more lines are too long

View File

@@ -39,6 +39,9 @@
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.1.4", "tsx": "^4.1.4",
"typescript": "^5.2.2" "typescript": "^5.2.2"
},
"engines": {
"node": ">=20.19.0 <21 || >=22.12.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {

View File

@@ -55,4 +55,7 @@
], ],
"author": "Recipe Admin", "author": "Recipe Admin",
"license": "MIT" "license": "MIT"
,"engines": {
"node": ">=20.19.0 <21 || >=22.12.0"
}
} }

View File

@@ -30,32 +30,37 @@ const limiter = rateLimit({
}); });
app.use(limiter); app.use(limiter);
// CORS configuration - Allow both development and production origins // CORS configuration - support comma separated origins in CORS_ORIGIN
const allowedOrigins = [ // NOTE: In docker-compose we temporarily set CORS_ORIGIN="*" for troubleshooting.
'http://localhost:5173', // Vite dev server // Narrow this down for production: e.g. CORS_ORIGIN="http://esprimo:3000,http://localhost:3000".
'http://localhost:3000', // Docker frontend let allowedOrigins: string[] = [];
config.cors.origin // Environment configured origin if (config.cors.origin === '*') {
].filter(Boolean); allowedOrigins = ['*'];
} else if (config.cors.origin.includes(',')) {
allowedOrigins = config.cors.origin.split(',').map(o => o.trim()).filter(Boolean);
} else {
allowedOrigins = [config.cors.origin];
}
// Add local network origins if CORS_ORIGIN is "*" (for local network access) // Always add defaults if not already present
const corsConfig = config.cors.origin === '*' ['http://localhost:5173','http://localhost:3000'].forEach(def => {
? { if (!allowedOrigins.includes(def) && !allowedOrigins.includes('*')) allowedOrigins.push(def);
origin: true, // Allow all origins for local network });
credentials: true,
}
: {
origin: allowedOrigins,
credentials: true,
};
app.use(cors(corsConfig)); app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true); // non-browser (curl, server-side)
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error(`CORS blocked for origin ${origin}`));
},
credentials: true,
}));
// Additional CORS headers for all requests // Additional CORS headers for all requests
app.use((req, res, next) => { app.use((req, res, next) => {
const origin = req.headers.origin; const origin = req.headers.origin;
if (config.cors.origin === '*') { if (allowedOrigins.includes('*')) {
// Allow all origins for local network access
res.header('Access-Control-Allow-Origin', origin || '*'); res.header('Access-Control-Allow-Origin', origin || '*');
} else if (origin && allowedOrigins.includes(origin)) { } else if (origin && allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Origin', origin);
@@ -107,8 +112,12 @@ app.get('/serve/*', (req, res, next) => {
} }
// Set headers for images // Set headers for images
const requestOrigin = req.headers.origin as string | undefined;
const chosenOrigin = allowedOrigins.includes('*')
? (requestOrigin || '*')
: (requestOrigin && allowedOrigins.includes(requestOrigin) ? requestOrigin : allowedOrigins[0] || 'http://localhost:3000');
res.set({ res.set({
'Access-Control-Allow-Origin': 'http://localhost:5173', 'Access-Control-Allow-Origin': chosenOrigin,
'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Credentials': 'true',
'Cache-Control': 'public, max-age=31536000', 'Cache-Control': 'public, max-age=31536000',
}); });

View File

@@ -4,6 +4,8 @@ services:
image: mysql:8.0 image: mysql:8.0
container_name: rezepte_mysql container_name: rezepte_mysql
restart: always restart: always
profiles:
- default
environment: environment:
MYSQL_ROOT_PASSWORD: rezepte123 MYSQL_ROOT_PASSWORD: rezepte123
MYSQL_DATABASE: rezepte MYSQL_DATABASE: rezepte
@@ -22,6 +24,8 @@ services:
build: . build: .
container_name: rezepte_app container_name: rezepte_app
restart: always restart: always
profiles:
- legacy
ports: ports:
- "8082:80" - "8082:80"
volumes: volumes:
@@ -41,6 +45,8 @@ services:
image: phpmyadmin:latest image: phpmyadmin:latest
container_name: rezepte_phpmyadmin container_name: rezepte_phpmyadmin
restart: always restart: always
profiles:
- admin
ports: ports:
- "8083:80" - "8083:80"
environment: environment:
@@ -53,8 +59,55 @@ services:
networks: networks:
- rezepte_network - rezepte_network
# Node.js Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: rezepte-backend
profiles:
- default
environment:
NODE_ENV: production
PORT: 3001
DATABASE_URL: mysql://rezepte_user:rezepte_pass@mysql:3306/rezepte
JWT_SECRET: your-super-secret-jwt-key-change-in-production
UPLOAD_PATH: /app/uploads
MAX_FILE_SIZE: 5242880
# CORS_ORIGIN: Restrict in production (example: http://esprimo:3000,http://localhost:3000)
CORS_ORIGIN: "*"
ports:
- "3001:3001"
volumes:
- uploads_data:/app/uploads
- ./uploads:/app/legacy-uploads:ro
networks:
- rezepte_network
depends_on:
- mysql
restart: unless-stopped
# Frontend (React + Vite build served by nginx)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
VITE_API_URL: http://localhost:3001/api
container_name: rezepte-frontend
profiles:
- default
ports:
- "3000:80"
networks:
- rezepte_network
depends_on:
- backend
restart: unless-stopped
volumes: volumes:
mysql_data: mysql_data:
uploads_data:
networks: networks:
rezepte_network: rezepte_network:

View File

@@ -1,5 +1,5 @@
# Frontend Dockerfile # Frontend Dockerfile
FROM node:18-alpine AS builder FROM node:22.12.0-alpine AS builder
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app

View File

@@ -26,6 +26,9 @@
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.43.0", "typescript-eslint": "^8.43.0",
"vite": "^7.1.6" "vite": "^7.1.6"
},
"engines": {
"node": ">=20.19.0 <21 || >=22.12.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {

View File

@@ -3,6 +3,9 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"engines": {
"node": ">=20.19.0 <21 || >=22.12.0"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",

View File

@@ -140,6 +140,15 @@
font-family: inherit; font-family: inherit;
transition: all 0.3s ease; transition: all 0.3s ease;
background: white; background: white;
color: #333; /* Ensure visible text color (fix for invisible typed characters) */
caret-color: #333;
}
/* Placeholder visibility */
.form-group input::placeholder,
.form-group textarea::placeholder {
color: #888;
opacity: 1; /* Override browser lower opacity */
} }
.form-group input:focus, .form-group input:focus,

View File

@@ -23,11 +23,12 @@ a:hover {
} }
body { body {
/* Removed global flex centering (display:flex + place-items:center) because it created a layout
that could interfere with stacking/focus of interactive form elements ("Neues Rezept" Eingabeproblem). */
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
/* Let the app container (see App.css .App) handle layout instead. */
} }
h1 { h1 {