Input-Text geht, CORS behoben
This commit is contained in:
6
.github/workflows/docker-build.yml
vendored
6
.github/workflows/docker-build.yml
vendored
@@ -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
.tool-versions
Normal file
1
.tool-versions
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node 22.12.0
|
||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
107
README.md
107
README.md
@@ -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
|
||||||
@@ -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 \
|
||||||
|
|||||||
2
backend/dist/app.d.ts.map
vendored
2
backend/dist/app.d.ts.map
vendored
@@ -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"}
|
||||||
16
backend/dist/app.js
vendored
16
backend/dist/app.js
vendored
@@ -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: true,
|
||||||
|
credentials: true,
|
||||||
|
}
|
||||||
|
: {
|
||||||
origin: allowedOrigins,
|
origin: allowedOrigins,
|
||||||
credentials: true,
|
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);
|
||||||
|
|||||||
2
backend/dist/app.js.map
vendored
2
backend/dist/app.js.map
vendored
@@ -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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"}
|
||||||
2
backend/dist/config/config.js.map
vendored
2
backend/dist/config/config.js.map
vendored
@@ -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"}
|
||||||
2
backend/dist/routes/health.js.map
vendored
2
backend/dist/routes/health.js.map
vendored
@@ -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"}
|
||||||
2
backend/dist/routes/images.d.ts.map
vendored
2
backend/dist/routes/images.d.ts.map
vendored
@@ -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"}
|
||||||
21
backend/dist/routes/images.js
vendored
21
backend/dist/routes/images.js
vendored
@@ -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',
|
||||||
});
|
});
|
||||||
|
|||||||
2
backend/dist/routes/images.js.map
vendored
2
backend/dist/routes/images.js.map
vendored
File diff suppressed because one or more lines are too long
3
backend/package-lock.json
generated
3
backend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -55,4 +55,7 @@
|
|||||||
],
|
],
|
||||||
"author": "Recipe Admin",
|
"author": "Recipe Admin",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
,"engines": {
|
||||||
|
"node": ">=20.19.0 <21 || >=22.12.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(',')) {
|
||||||
// Add local network origins if CORS_ORIGIN is "*" (for local network access)
|
allowedOrigins = config.cors.origin.split(',').map(o => o.trim()).filter(Boolean);
|
||||||
const corsConfig = config.cors.origin === '*'
|
} else {
|
||||||
? {
|
allowedOrigins = [config.cors.origin];
|
||||||
origin: true, // Allow all origins for local network
|
|
||||||
credentials: true,
|
|
||||||
}
|
}
|
||||||
: {
|
|
||||||
origin: allowedOrigins,
|
|
||||||
credentials: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use(cors(corsConfig));
|
// Always add defaults if not already present
|
||||||
|
['http://localhost:5173','http://localhost:3000'].forEach(def => {
|
||||||
|
if (!allowedOrigins.includes(def) && !allowedOrigins.includes('*')) allowedOrigins.push(def);
|
||||||
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
3
frontend/package-lock.json
generated
3
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user