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
|
||||
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
|
||||
uses: docker/login-action@v3
|
||||
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
|
||||
|
||||
### ⚙️ 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)
|
||||
```bash
|
||||
# From main project directory
|
||||
@@ -42,6 +58,7 @@ docker-compose up -d
|
||||
### 2. Start the Node.js Backend
|
||||
```bash
|
||||
cd backend
|
||||
nvm use # optional, falls nvm
|
||||
npm install
|
||||
npm run build
|
||||
node dist/app.js
|
||||
@@ -50,6 +67,7 @@ node dist/app.js
|
||||
### 3. Start the React Frontend
|
||||
```bash
|
||||
cd frontend
|
||||
nvm use # optional
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
115
README.md
115
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)
|
||||
- **MySQL**: Datenbankserver (Port 3307)
|
||||
- **phpMyAdmin**: Datenbankadministration (Port 8083)
|
||||
| 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 |
|
||||
| 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:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Lokal ohne Docker (Entwicklung Hot Reload):
|
||||
```bash
|
||||
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:**
|
||||
- Rezepte-App: http://localhost:8082
|
||||
- phpMyAdmin: http://localhost:8083
|
||||
## Docker Nutzung
|
||||
|
||||
3. **Container stoppen:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
### Standard (nur moderner Stack: DB + API + Frontend)
|
||||
```bash
|
||||
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
|
||||
|
||||
@@ -41,23 +96,16 @@ Eine dockerisierte Version der Rezepte-Verwaltungsanwendung mit PHP, MySQL und p
|
||||
- Username: root
|
||||
- Password: rezepte123
|
||||
|
||||
## Entwicklung
|
||||
## Entwicklung innerhalb Docker
|
||||
|
||||
### Container neu bauen:
|
||||
Status:
|
||||
```bash
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
### Logs anzeigen:
|
||||
Direktes DB Login (Host hat mysql Client):
|
||||
```bash
|
||||
docker-compose logs -f php-app
|
||||
docker-compose logs -f mysql
|
||||
```
|
||||
|
||||
### Container Status:
|
||||
```bash
|
||||
docker-compose ps
|
||||
mysql -h 127.0.0.1 -P 3307 -u rezepte_user -p'rezepte_pass' rezepte
|
||||
```
|
||||
|
||||
## Datenvolumes
|
||||
@@ -66,4 +114,9 @@ Die MySQL-Daten werden in einem Docker-Volume gespeichert und bleiben auch nach
|
||||
|
||||
## 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
|
||||
FROM node:18-alpine AS builder
|
||||
FROM node:22.12.0-alpine AS builder
|
||||
|
||||
# Install OpenSSL for Prisma compatibility
|
||||
RUN apk add --no-cache openssl openssl-dev
|
||||
@@ -20,7 +20,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# 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
|
||||
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"}
|
||||
20
backend/dist/app.js
vendored
20
backend/dist/app.js
vendored
@@ -32,13 +32,22 @@ const allowedOrigins = [
|
||||
'http://localhost:3000',
|
||||
config_1.config.cors.origin
|
||||
].filter(Boolean);
|
||||
app.use((0, cors_1.default)({
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
}));
|
||||
const corsConfig = config_1.config.cors.origin === '*'
|
||||
? {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
}
|
||||
: {
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
};
|
||||
app.use((0, cors_1.default)(corsConfig));
|
||||
app.use((req, res, next) => {
|
||||
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-Credentials', 'true');
|
||||
@@ -52,6 +61,7 @@ app.use((req, res, next) => {
|
||||
app.use(express_1.default.json({ limit: '10mb' }));
|
||||
app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' }));
|
||||
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/recipes', recipes_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 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 storage = multer_1.default.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const recipeNumber = req.body.recipeNumber || req.params.recipeNumber;
|
||||
if (!recipeNumber) {
|
||||
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)) {
|
||||
fs_1.default.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
@@ -28,7 +36,7 @@ const storage = multer_1.default.diskStorage({
|
||||
if (!recipeNumber) {
|
||||
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)
|
||||
? 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 fullPath = path_1.default.join(process.cwd(), '../../uploads', cleanPath);
|
||||
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}`);
|
||||
@@ -182,8 +190,13 @@ router.get('/serve/:imagePath(*)', (req, res, next) => {
|
||||
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': 'http://localhost:5173',
|
||||
'Access-Control-Allow-Origin': corsOrigin,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'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",
|
||||
"tsx": "^4.1.4",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0 <21 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
||||
@@ -55,4 +55,7 @@
|
||||
],
|
||||
"author": "Recipe Admin",
|
||||
"license": "MIT"
|
||||
,"engines": {
|
||||
"node": ">=20.19.0 <21 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,32 +30,37 @@ const limiter = rateLimit({
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// CORS configuration - Allow both development and production origins
|
||||
const allowedOrigins = [
|
||||
'http://localhost:5173', // Vite dev server
|
||||
'http://localhost:3000', // Docker frontend
|
||||
config.cors.origin // Environment configured origin
|
||||
].filter(Boolean);
|
||||
// CORS configuration - support comma separated origins in CORS_ORIGIN
|
||||
// NOTE: In docker-compose we temporarily set CORS_ORIGIN="*" for troubleshooting.
|
||||
// Narrow this down for production: e.g. CORS_ORIGIN="http://esprimo:3000,http://localhost:3000".
|
||||
let allowedOrigins: string[] = [];
|
||||
if (config.cors.origin === '*') {
|
||||
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)
|
||||
const corsConfig = config.cors.origin === '*'
|
||||
? {
|
||||
origin: true, // Allow all origins for local network
|
||||
credentials: true,
|
||||
}
|
||||
: {
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
};
|
||||
// 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(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
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
|
||||
if (config.cors.origin === '*') {
|
||||
// Allow all origins for local network access
|
||||
if (allowedOrigins.includes('*')) {
|
||||
res.header('Access-Control-Allow-Origin', origin || '*');
|
||||
} else if (origin && allowedOrigins.includes(origin)) {
|
||||
res.header('Access-Control-Allow-Origin', origin);
|
||||
@@ -107,8 +112,12 @@ app.get('/serve/*', (req, res, next) => {
|
||||
}
|
||||
|
||||
// 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({
|
||||
'Access-Control-Allow-Origin': 'http://localhost:5173',
|
||||
'Access-Control-Allow-Origin': chosenOrigin,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ services:
|
||||
image: mysql:8.0
|
||||
container_name: rezepte_mysql
|
||||
restart: always
|
||||
profiles:
|
||||
- default
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rezepte123
|
||||
MYSQL_DATABASE: rezepte
|
||||
@@ -22,6 +24,8 @@ services:
|
||||
build: .
|
||||
container_name: rezepte_app
|
||||
restart: always
|
||||
profiles:
|
||||
- legacy
|
||||
ports:
|
||||
- "8082:80"
|
||||
volumes:
|
||||
@@ -41,6 +45,8 @@ services:
|
||||
image: phpmyadmin:latest
|
||||
container_name: rezepte_phpmyadmin
|
||||
restart: always
|
||||
profiles:
|
||||
- admin
|
||||
ports:
|
||||
- "8083:80"
|
||||
environment:
|
||||
@@ -53,8 +59,55 @@ services:
|
||||
networks:
|
||||
- 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:
|
||||
mysql_data:
|
||||
uploads_data:
|
||||
|
||||
networks:
|
||||
rezepte_network:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Frontend Dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
FROM node:22.12.0-alpine AS builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
3
frontend/package-lock.json
generated
3
frontend/package-lock.json
generated
@@ -26,6 +26,9 @@
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"vite": "^7.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0 <21 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.19.0 <21 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
|
||||
@@ -140,6 +140,15 @@
|
||||
font-family: inherit;
|
||||
transition: all 0.3s ease;
|
||||
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,
|
||||
|
||||
@@ -23,11 +23,12 @@ a:hover {
|
||||
}
|
||||
|
||||
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;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
/* Let the app container (see App.css .App) handle layout instead. */
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
Reference in New Issue
Block a user