Umstellung auf E-Mail-System und Multi-Site ohne Datenbank
This commit is contained in:
parent
e0181c2b5d
commit
908b4d6ad1
45
.env.example
45
.env.example
@ -1,7 +1,44 @@
|
||||
# Copy this file to .env and fill in your actual values
|
||||
|
||||
# Database Configuration (Neon PostgreSQL)
|
||||
DATABASE_URL="postgresql://username:password@hostname:5432/database?sslmode=require"
|
||||
# E-Mail-Konfiguration für RSVP-Versendung
|
||||
SMTP_HOST=mail.ihrer-domain.de
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=noreply@ihre-domain.de
|
||||
SMTP_PASS=ihr-smtp-passwort
|
||||
SMTP_FROM=RSVP System <noreply@ihre-domain.de>
|
||||
NOTIFICATION_EMAIL=hochzeit@ihre-domain.de
|
||||
|
||||
# Development settings
|
||||
NODE_ENV=development
|
||||
# Multi-Site Konfiguration (JSON Format)
|
||||
# Beispiel für mehrere Sites:
|
||||
SITES_CONFIG='{
|
||||
"ed.pixelbrew.de": {
|
||||
"domain": "ed.pixelbrew.de",
|
||||
"title": "Hochzeit Eillen & Dennis",
|
||||
"subtitle": "Gutshof Sonenberg • 2026",
|
||||
"email": "hochzeit@pixelbrew.de",
|
||||
"theme": {
|
||||
"primaryColor": "#d4a574",
|
||||
"secondaryColor": "#f4e4bc",
|
||||
"backgroundColor": "#f9f9f9"
|
||||
},
|
||||
"auth": {
|
||||
"credentials": [
|
||||
{ "username": "hochzeit", "password": "eillen2026" },
|
||||
{ "username": "gast", "password": "dennis2026" },
|
||||
{ "username": "einladung", "password": "gutsonenberg" }
|
||||
],
|
||||
"realm": "Hochzeit Eillen & Dennis - Nur für eingeladene Gäste"
|
||||
},
|
||||
"content": {
|
||||
"welcomeMessage": "Willkommen zur Hochzeit von Eillen & Dennis",
|
||||
"rsvpEnabled": true,
|
||||
"eventDate": "2026",
|
||||
"eventLocation": "Gutshof Sonenberg"
|
||||
}
|
||||
}
|
||||
}'
|
||||
|
||||
# Entwicklungsumgebung
|
||||
NODE_ENV=production
|
||||
PORT=5000
|
||||
|
||||
44
Dockerfile
44
Dockerfile
@ -1,44 +0,0 @@
|
||||
# --------- BUILD STAGE ---------
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy the rest of the source code
|
||||
COPY . .
|
||||
|
||||
# Build frontend (Vite) and backend (esbuild)
|
||||
RUN npm run build
|
||||
|
||||
# --------- RUNTIME STAGE ---------
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only what is needed to run the app
|
||||
COPY --from=builder /app/package*.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Create a non-root user
|
||||
RUN addgroup -g 1001 -S nodejs \
|
||||
&& adduser -S nextjs -u 1001 \
|
||||
&& chown -R nextjs:nodejs /app
|
||||
|
||||
USER nextjs
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5000
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
# Healthcheck to verify app is up
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --spider -q http://localhost:5000/ || exit 1
|
||||
|
||||
# Start the backend (your dist/index.js)
|
||||
CMD ["node", "dist/index.js"]
|
||||
109
MIGRATION_COMPLETE.md
Normal file
109
MIGRATION_COMPLETE.md
Normal file
@ -0,0 +1,109 @@
|
||||
# 🎊 Wedding RSVP - Umstellung auf Server erfolgreich!
|
||||
|
||||
## ✅ Was wurde erfolgreich umgestellt:
|
||||
|
||||
### 🗑️ Entfernt (nicht mehr benötigt):
|
||||
- ❌ **Docker** - docker-compose.yml und Dockerfile entfernt
|
||||
- ❌ **PostgreSQL/Neon Database** - drizzle.config.ts und db.ts entfernt
|
||||
- ❌ **Vercel** - vercel.json entfernt
|
||||
- ❌ **Drizzle ORM** - Abhängigkeiten aus package.json entfernt
|
||||
|
||||
### ✅ Neu implementiert:
|
||||
|
||||
#### 📧 E-Mail-basierte RSVP-Verarbeitung
|
||||
- **E-Mail-Service** (`server/email.ts`) - Versendet RSVP-Anmeldungen per E-Mail
|
||||
- **Nodemailer** - Professionelle E-Mail-Versendung über SMTP
|
||||
- **HTML + Text E-Mails** - Schöne formatierte Benachrichtigungen
|
||||
|
||||
#### 🌍 Multi-Site-Unterstützung
|
||||
- **Site-Configuration-Manager** (`server/site-config.ts`) - Verwaltet mehrere Websites
|
||||
- **Domain-basierte Isolation** - Jede Domain hat eigene Einstellungen
|
||||
- **Individuelle Authentication** - Separate Zugangsdaten pro Site
|
||||
- **Anpassbare Themes** - Eigene Farben und Texte pro Hochzeit
|
||||
|
||||
#### 🚀 Server-optimiert
|
||||
- **Nginx-Konfiguration** - Reverse Proxy mit SSL-Unterstützung
|
||||
- **Systemd-Service** - Automatischer Start und Überwachung
|
||||
- **Deployment-Scripts** - Einfache Bereitstellung
|
||||
- **Security Headers** - HTTPS, HSTS, Content-Security etc.
|
||||
|
||||
## 📁 Neue Dateien:
|
||||
|
||||
### Server-Code:
|
||||
- `server/email.ts` - E-Mail-Versendung
|
||||
- `server/site-config.ts` - Multi-Site-Verwaltung
|
||||
- `server/storage.ts` - Ersetzt Datenbank durch E-Mail
|
||||
- `shared/schema.ts` - Vereinfachte Schemas ohne DB
|
||||
|
||||
### Deployment:
|
||||
- `deployment/README.md` - Vollständige Server-Setup-Anleitung
|
||||
- `deployment/nginx-wedding-sites.conf` - Nginx-Konfiguration
|
||||
- `deployment/pixelbrew-wedding.service` - Systemd-Service
|
||||
- `deployment/deploy.sh` - Deployment-Script
|
||||
- `.env.example` - Aktualisierte Umgebungsvariablen
|
||||
|
||||
## 🔧 Wichtige Konfigurationsänderungen:
|
||||
|
||||
### 📦 package.json
|
||||
- Entfernt: `@neondatabase/serverless`, `drizzle-orm`, `drizzle-zod`, `drizzle-kit`
|
||||
- Hinzugefügt: `nodemailer`, `@types/nodemailer`
|
||||
- Aktualisiert: Build-Scripts für Server-Deployment
|
||||
|
||||
### 🛠️ Build-System
|
||||
- `tsconfig.server.json` - Separate TypeScript-Konfiguration für Server
|
||||
- Aktualisierte Build-Scripts für Produktions-Deployment
|
||||
- ES Module-kompatible Builds
|
||||
|
||||
## 🌐 Multi-Site Beispiel-Konfiguration:
|
||||
|
||||
```json
|
||||
{
|
||||
"ed.pixelbrew.de": {
|
||||
"title": "Hochzeit Eillen & Dennis",
|
||||
"email": "hochzeit@pixelbrew.de",
|
||||
"theme": {"primaryColor": "#d4a574"},
|
||||
"auth": {
|
||||
"credentials": [
|
||||
{"username": "hochzeit", "password": "eillen2026"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"anna-max.ihre-domain.de": {
|
||||
"title": "Hochzeit Anna & Max",
|
||||
"email": "anna-max@ihre-domain.de",
|
||||
"theme": {"primaryColor": "#8b5a3c"},
|
||||
"auth": {
|
||||
"credentials": [
|
||||
{"username": "hochzeit", "password": "anna2026"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📧 E-Mail-Funktionen:
|
||||
|
||||
- **Automatische RSVP-Benachrichtigungen** - Bei jeder Anmeldung
|
||||
- **HTML + Text Format** - Schöne Darstellung in allen E-Mail-Clients
|
||||
- **Site-spezifische Templates** - Angepasst an Hochzeits-Theme
|
||||
- **SMTP-Konfigurierbar** - Funktioniert mit jedem E-Mail-Provider
|
||||
|
||||
## 🚀 Nächste Schritte für Server-Deployment:
|
||||
|
||||
1. **Server vorbereiten** - Node.js, nginx, certbot installieren
|
||||
2. **Repository klonen** - Code auf Server kopieren
|
||||
3. **E-Mail konfigurieren** - SMTP-Einstellungen in .env
|
||||
4. **Multi-Site Setup** - Domains in SITES_CONFIG definieren
|
||||
5. **SSL-Zertifikate** - Let's Encrypt für jede Domain
|
||||
6. **Service starten** - systemd-Service aktivieren
|
||||
|
||||
Die komplette Anleitung finden Sie in `deployment/README.md`!
|
||||
|
||||
## 🔒 Sicherheits-Features:
|
||||
|
||||
- ✅ **Domain-Isolation** - Jede Site hat eigene Auth-Daten
|
||||
- ✅ **HTTPS-only** - Automatische SSL-Zertifikate
|
||||
- ✅ **Security Headers** - HSTS, XSS-Protection, etc.
|
||||
- ✅ **Minimale Permissions** - Systemd-Service mit eingeschränkten Rechten
|
||||
|
||||
**Ihre Wedding RSVP-Anwendung ist jetzt bereit für den Server-Betrieb! 🎉**
|
||||
217
deployment/README.md
Normal file
217
deployment/README.md
Normal file
@ -0,0 +1,217 @@
|
||||
# Wedding RSVP Multi-Site Server Setup
|
||||
|
||||
Diese Anwendung wurde für den Betrieb auf einem eigenen Server mit nginx und Certbot umgestellt.
|
||||
|
||||
## ✅ Entfernte Abhängigkeiten
|
||||
- ❌ Docker / Docker Compose
|
||||
- ❌ PostgreSQL/Neon Database
|
||||
- ❌ Vercel Deployment
|
||||
- ❌ Drizzle ORM
|
||||
|
||||
## ✅ Neue Features
|
||||
- ✉️ E-Mail-basierte RSVP-Verarbeitung (statt Datenbank)
|
||||
- 🌍 Multi-Site-Unterstützung mit Domain-basierter Isolation
|
||||
- 🔒 Individuelle Authentifizierung pro Site
|
||||
- 🎨 Anpassbare Themes pro Site
|
||||
|
||||
## Server-Anforderungen
|
||||
|
||||
- Ubuntu/Debian Linux Server
|
||||
- Node.js 18+
|
||||
- nginx
|
||||
- Certbot (Let's Encrypt)
|
||||
- SMTP-Server für E-Mail-Versand
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. System-Dependencies installieren
|
||||
```bash
|
||||
# Node.js installieren
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# nginx installieren
|
||||
sudo apt-get install nginx
|
||||
|
||||
# Certbot installieren
|
||||
sudo apt-get install certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
### 2. Anwendung einrichten
|
||||
```bash
|
||||
# Repository klonen
|
||||
sudo mkdir -p /var/www/ed.pixelbrew.de
|
||||
sudo git clone https://github.com/ihr-username/wedding-rsvp.git /var/www/ed.pixelbrew.de
|
||||
cd /var/www/ed.pixelbrew.de
|
||||
|
||||
# Dependencies installieren
|
||||
npm install
|
||||
|
||||
# Umgebungsvariablen konfigurieren
|
||||
cp .env.example .env
|
||||
sudo nano .env
|
||||
```
|
||||
|
||||
### 3. E-Mail-Konfiguration (.env)
|
||||
```bash
|
||||
# SMTP-Server Konfiguration
|
||||
SMTP_HOST=mail.ihre-domain.de
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=noreply@ihre-domain.de
|
||||
SMTP_PASS=ihr-smtp-passwort
|
||||
NOTIFICATION_EMAIL=hochzeit@ihre-domain.de
|
||||
```
|
||||
|
||||
### 4. Multi-Site Konfiguration (.env)
|
||||
```bash
|
||||
# JSON-Format für mehrere Sites
|
||||
SITES_CONFIG='{
|
||||
"ed.pixelbrew.de": {
|
||||
"title": "Hochzeit Eillen & Dennis",
|
||||
"email": "hochzeit@pixelbrew.de",
|
||||
"theme": {"primaryColor": "#d4a574"},
|
||||
"auth": {
|
||||
"credentials": [
|
||||
{"username": "hochzeit", "password": "eillen2026"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 5. Systemd Service einrichten
|
||||
```bash
|
||||
# Service-Datei kopieren
|
||||
sudo cp deployment/pixelbrew-wedding.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable pixelbrew-wedding
|
||||
```
|
||||
|
||||
### 6. nginx Konfiguration
|
||||
```bash
|
||||
# nginx-Konfiguration kopieren
|
||||
sudo cp deployment/nginx-wedding-sites.conf /etc/nginx/sites-available/wedding-sites
|
||||
sudo ln -s /etc/nginx/sites-available/wedding-sites /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 7. SSL-Zertifikate generieren
|
||||
```bash
|
||||
# Für jede Domain SSL-Zertifikat erstellen
|
||||
sudo certbot --nginx -d ed.pixelbrew.de
|
||||
sudo certbot --nginx -d weitere-domain.de
|
||||
```
|
||||
|
||||
### 8. Deployment ausführen
|
||||
```bash
|
||||
# Anwendung bauen und starten
|
||||
chmod +x deployment/deploy.sh
|
||||
./deployment/deploy.sh ed.pixelbrew.de
|
||||
```
|
||||
|
||||
## Neue Site hinzufügen
|
||||
|
||||
### 1. Domain zur nginx-Konfiguration hinzufügen
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/wedding-sites
|
||||
# Neuen server-Block für die Domain hinzufügen
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 2. SSL-Zertifikat für neue Domain
|
||||
```bash
|
||||
sudo certbot --nginx -d neue-domain.de
|
||||
```
|
||||
|
||||
### 3. Site-Konfiguration in .env erweitern
|
||||
```bash
|
||||
sudo nano /var/www/ed.pixelbrew.de/.env
|
||||
# SITES_CONFIG JSON um neue Domain erweitern
|
||||
```
|
||||
|
||||
### 4. Service neu starten
|
||||
```bash
|
||||
sudo systemctl restart pixelbrew-wedding
|
||||
```
|
||||
|
||||
## Monitoring & Logs
|
||||
|
||||
### Service-Status prüfen
|
||||
```bash
|
||||
sudo systemctl status pixelbrew-wedding
|
||||
```
|
||||
|
||||
### Logs ansehen
|
||||
```bash
|
||||
# Application Logs
|
||||
sudo journalctl -u pixelbrew-wedding -f
|
||||
|
||||
# nginx Access Logs
|
||||
sudo tail -f /var/log/nginx/ed.pixelbrew.de.access.log
|
||||
|
||||
# nginx Error Logs
|
||||
sudo tail -f /var/log/nginx/ed.pixelbrew.de.error.log
|
||||
```
|
||||
|
||||
### E-Mail-Test
|
||||
```bash
|
||||
# Ins Application-Verzeichnis wechseln
|
||||
cd /var/www/ed.pixelbrew.de
|
||||
|
||||
# E-Mail-Verbindung testen (falls Test-Endpoint implementiert)
|
||||
curl -X POST https://ed.pixelbrew.de/api/test-email
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
### Wichtige Dateien sichern
|
||||
```bash
|
||||
# .env Datei (enthält alle Konfigurationen)
|
||||
sudo cp /var/www/ed.pixelbrew.de/.env ~/backup/
|
||||
|
||||
# nginx Konfiguration
|
||||
sudo cp /etc/nginx/sites-available/wedding-sites ~/backup/
|
||||
|
||||
# SSL-Zertifikate (optional, werden automatisch erneuert)
|
||||
sudo cp -r /etc/letsencrypt ~/backup/
|
||||
```
|
||||
|
||||
## Sicherheits-Features
|
||||
|
||||
- ✅ HTTP Basic Auth pro Site
|
||||
- ✅ HTTPS-only mit automatischen SSL-Zertifikaten
|
||||
- ✅ Security Headers (HSTS, X-Frame-Options, etc.)
|
||||
- ✅ Domain-basierte Isolation
|
||||
- ✅ Minimale Systemd-Permissions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service startet nicht
|
||||
```bash
|
||||
# Logs prüfen
|
||||
sudo journalctl -u pixelbrew-wedding --no-pager
|
||||
|
||||
# Konfiguration testen
|
||||
cd /var/www/ed.pixelbrew.de
|
||||
npm run check
|
||||
```
|
||||
|
||||
### nginx Fehler
|
||||
```bash
|
||||
# Konfiguration testen
|
||||
sudo nginx -t
|
||||
|
||||
# Error Logs prüfen
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
### E-Mail wird nicht versendet
|
||||
```bash
|
||||
# SMTP-Verbindung testen
|
||||
telnet ihr-smtp-server.de 587
|
||||
|
||||
# .env Konfiguration prüfen
|
||||
sudo nano /var/www/ed.pixelbrew.de/.env
|
||||
```
|
||||
64
deployment/deploy.sh
Normal file
64
deployment/deploy.sh
Normal file
@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deployment Skript für Wedding RSVP Multi-Site Anwendung
|
||||
# Verwendung: ./deploy.sh [domain]
|
||||
|
||||
set -e
|
||||
|
||||
DOMAIN=${1:-"ed.pixelbrew.de"}
|
||||
APP_DIR="/var/www/$DOMAIN"
|
||||
SERVICE_NAME="pixelbrew-wedding"
|
||||
USER="www-data"
|
||||
|
||||
echo "🚀 Deploying Wedding RSVP Application for $DOMAIN..."
|
||||
|
||||
# 1. Repository aktualisieren
|
||||
echo "📥 Updating repository..."
|
||||
cd "$APP_DIR"
|
||||
git pull origin main
|
||||
|
||||
# 2. Dependencies installieren
|
||||
echo "📦 Installing dependencies..."
|
||||
npm ci --production=false
|
||||
|
||||
# 3. Application bauen
|
||||
echo "🔨 Building application..."
|
||||
npm run build
|
||||
|
||||
# 4. Service stoppen
|
||||
echo "⏹️ Stopping service..."
|
||||
sudo systemctl stop $SERVICE_NAME
|
||||
|
||||
# 5. Production dependencies installieren
|
||||
echo "📦 Installing production dependencies..."
|
||||
cd dist
|
||||
npm ci --production=true
|
||||
|
||||
# 6. Permissions setzen
|
||||
echo "🔐 Setting permissions..."
|
||||
sudo chown -R $USER:$USER "$APP_DIR"
|
||||
sudo chmod -R 755 "$APP_DIR"
|
||||
|
||||
# 7. Service starten
|
||||
echo "▶️ Starting service..."
|
||||
sudo systemctl start $SERVICE_NAME
|
||||
sudo systemctl enable $SERVICE_NAME
|
||||
|
||||
# 8. Status prüfen
|
||||
echo "🔍 Checking service status..."
|
||||
sleep 3
|
||||
sudo systemctl status $SERVICE_NAME --no-pager
|
||||
|
||||
# 9. Nginx Konfiguration neu laden (falls geändert)
|
||||
echo "🌐 Reloading nginx configuration..."
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
echo "✅ Deployment completed successfully!"
|
||||
echo "🌐 Site verfügbar unter: https://$DOMAIN"
|
||||
|
||||
# 10. Log-Zugriff
|
||||
echo ""
|
||||
echo "📄 Logs anzeigen mit:"
|
||||
echo "sudo journalctl -u $SERVICE_NAME -f"
|
||||
echo "sudo tail -f /var/log/nginx/$DOMAIN.access.log"
|
||||
echo "sudo tail -f /var/log/nginx/$DOMAIN.error.log"
|
||||
110
deployment/nginx-wedding-sites.conf
Normal file
110
deployment/nginx-wedding-sites.conf
Normal file
@ -0,0 +1,110 @@
|
||||
# Nginx Konfiguration für Multi-Site Wedding RSVP
|
||||
# Speichern als: /etc/nginx/sites-available/wedding-sites
|
||||
|
||||
# Upstream für die Node.js-Anwendung
|
||||
upstream wedding_app {
|
||||
server 127.0.0.1:5000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP zu HTTPS Redirect
|
||||
server {
|
||||
listen 80;
|
||||
server_name ed.pixelbrew.de *.ihre-domain.de;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# Hauptseite: ed.pixelbrew.de
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name ed.pixelbrew.de;
|
||||
|
||||
# SSL-Konfiguration (von Certbot verwaltet)
|
||||
ssl_certificate /etc/letsencrypt/live/ed.pixelbrew.de/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/ed.pixelbrew.de/privkey.pem;
|
||||
|
||||
# SSL-Security Headers
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# Security Headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Proxy zur Node.js-Anwendung
|
||||
location / {
|
||||
proxy_pass http://wedding_app;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_redirect off;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Statische Dateien cachen
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
|
||||
proxy_pass http://wedding_app;
|
||||
proxy_cache_valid 200 1h;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
# Logs
|
||||
access_log /var/log/nginx/ed.pixelbrew.de.access.log;
|
||||
error_log /var/log/nginx/ed.pixelbrew.de.error.log;
|
||||
}
|
||||
|
||||
# Template für weitere Wedding-Sites
|
||||
# Kopieren und anpassen für jede neue Domain
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name beispiel.ihre-domain.de;
|
||||
#
|
||||
# ssl_certificate /etc/letsencrypt/live/beispiel.ihre-domain.de/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/beispiel.ihre-domain.de/privkey.pem;
|
||||
#
|
||||
# # SSL und Security Headers (wie oben)
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
# ssl_prefer_server_ciphers off;
|
||||
#
|
||||
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
# add_header X-Frame-Options DENY always;
|
||||
# add_header X-Content-Type-Options nosniff always;
|
||||
# add_header X-XSS-Protection "1; mode=block" always;
|
||||
# add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://wedding_app;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection 'upgrade';
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_cache_bypass $http_upgrade;
|
||||
# proxy_redirect off;
|
||||
# }
|
||||
#
|
||||
# location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
|
||||
# proxy_pass http://wedding_app;
|
||||
# proxy_cache_valid 200 1h;
|
||||
# add_header Cache-Control "public, max-age=3600";
|
||||
# }
|
||||
#
|
||||
# access_log /var/log/nginx/beispiel.ihre-domain.de.access.log;
|
||||
# error_log /var/log/nginx/beispiel.ihre-domain.de.error.log;
|
||||
# }
|
||||
24
deployment/pixelbrew-wedding.service
Normal file
24
deployment/pixelbrew-wedding.service
Normal file
@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Pixelbrew Wedding RSVP Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/var/www/ed.pixelbrew.de
|
||||
Environment=NODE_ENV=production
|
||||
EnvironmentFile=/var/www/ed.pixelbrew.de/.env
|
||||
ExecStart=/usr/bin/node dist/server/index.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Security settings
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/www/ed.pixelbrew.de
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -1,22 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
hochzeit-app:
|
||||
build: .
|
||||
container_name: hochzeit-app
|
||||
restart: always
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=5000
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hochzeit.rule=Host(`hochzeit.deine-domain.de`)"
|
||||
- "traefik.http.routers.hochzeit.entrypoints=websecure"
|
||||
- "traefik.http.routers.hochzeit.tls.certresolver=myresolver"
|
||||
- "traefik.http.services.hochzeit.loadbalancer.server.port=5000"
|
||||
networks:
|
||||
- trafik
|
||||
|
||||
networks:
|
||||
treafik:
|
||||
external: true
|
||||
@ -1,14 +0,0 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error("DATABASE_URL, ensure the database is provisioned");
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
out: "./migrations",
|
||||
schema: "./shared/schema.ts",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
});
|
||||
1577
package-lock.json
generated
1577
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -5,15 +5,14 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development tsx server/index.ts",
|
||||
"build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
|
||||
"start": "NODE_ENV=production node dist/index.js",
|
||||
"check": "tsc",
|
||||
"db:push": "drizzle-kit push"
|
||||
"build": "vite build && npm run build:server",
|
||||
"build:server": "tsc --project tsconfig.server.json && copy package.json dist\\",
|
||||
"start": "NODE_ENV=production node dist/server/index.js",
|
||||
"check": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"@neondatabase/serverless": "^0.10.4",
|
||||
"@radix-ui/react-accordion": "^1.2.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||
@ -49,8 +48,6 @@
|
||||
"connect-pg-simple": "^10.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-zod": "^0.7.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1",
|
||||
@ -60,6 +57,7 @@
|
||||
"memoizee": "^0.4.17",
|
||||
"memorystore": "^1.6.7",
|
||||
"next-themes": "^0.4.6",
|
||||
"nodemailer": "^6.10.1",
|
||||
"openid-client": "^6.6.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-local": "^1.0.0",
|
||||
@ -88,6 +86,7 @@
|
||||
"@types/express": "4.17.21",
|
||||
"@types/express-session": "^1.18.0",
|
||||
"@types/node": "20.16.11",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/react": "^18.3.11",
|
||||
@ -95,8 +94,6 @@
|
||||
"@types/ws": "^8.5.13",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"esbuild": "^0.25.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsx": "^4.19.1",
|
||||
|
||||
15
server/db.ts
15
server/db.ts
@ -1,15 +0,0 @@
|
||||
import { Pool, neonConfig } from '@neondatabase/serverless';
|
||||
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||
import ws from "ws";
|
||||
import * as schema from "@shared/schema";
|
||||
|
||||
neonConfig.webSocketConstructor = ws;
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error(
|
||||
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||
);
|
||||
}
|
||||
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
export const db = drizzle({ client: pool, schema });
|
||||
133
server/email.ts
Normal file
133
server/email.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { type InsertRsvp } from '../shared/schema';
|
||||
|
||||
export class EmailService {
|
||||
private transporter: nodemailer.Transporter;
|
||||
constructor() {
|
||||
// SMTP Konfiguration über Umgebungsvariablen
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'localhost',
|
||||
port: parseInt(process.env.SMTP_PORT || '587'),
|
||||
secure: process.env.SMTP_SECURE === 'true', // true für 465, false für andere Ports
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async sendRsvpNotification(rsvp: InsertRsvp, siteConfig: any): Promise<void> {
|
||||
const attendanceText = {
|
||||
'ja': '✅ Kommt zur Hochzeit',
|
||||
'nein': '❌ Kann leider nicht kommen',
|
||||
'vielleicht': '❓ Ist sich noch unsicher'
|
||||
};
|
||||
|
||||
const menuText = {
|
||||
'fleisch': '🥩 Fleisch',
|
||||
'vegetarisch': '🥗 Vegetarisch',
|
||||
'vegan': '🌱 Vegan'
|
||||
};
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neue RSVP - ${siteConfig.title}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: linear-gradient(135deg, #d4a574, #f4e4bc); padding: 30px; text-align: center; border-radius: 10px; margin-bottom: 30px; }
|
||||
.header h1 { color: white; margin: 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
|
||||
.content { background: #f9f9f9; padding: 25px; border-radius: 10px; }
|
||||
.field { margin-bottom: 15px; padding: 10px; background: white; border-radius: 5px; border-left: 4px solid #d4a574; }
|
||||
.field strong { color: #d4a574; }
|
||||
.footer { text-align: center; margin-top: 30px; color: #666; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>💒 Neue RSVP-Anmeldung</h1>
|
||||
<p style="color: white; margin: 10px 0 0 0;">${siteConfig.title}</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="field">
|
||||
<strong>👤 Name:</strong><br>
|
||||
${rsvp.name}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>📅 Teilnahme:</strong><br>
|
||||
${attendanceText[rsvp.attendance as keyof typeof attendanceText]}
|
||||
</div>
|
||||
|
||||
${rsvp.menu ? `
|
||||
<div class="field">
|
||||
<strong>🍽️ Menüwahl:</strong><br>
|
||||
${menuText[rsvp.menu as keyof typeof menuText]}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${rsvp.allergies ? `
|
||||
<div class="field">
|
||||
<strong>⚠️ Allergien/Hinweise:</strong><br>
|
||||
${rsvp.allergies}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="field">
|
||||
<strong>📧 Eingegangen am:</strong><br>
|
||||
${new Date().toLocaleString('de-DE')}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<strong>🌐 Website:</strong><br>
|
||||
${siteConfig.domain}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Diese E-Mail wurde automatisch vom RSVP-System generiert.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const textContent = `
|
||||
Neue RSVP-Anmeldung für ${siteConfig.title}
|
||||
|
||||
👤 Name: ${rsvp.name}
|
||||
📅 Teilnahme: ${attendanceText[rsvp.attendance as keyof typeof attendanceText]}
|
||||
${rsvp.menu ? `🍽️ Menüwahl: ${menuText[rsvp.menu as keyof typeof menuText]}` : ''}
|
||||
${rsvp.allergies ? `⚠️ Allergien/Hinweise: ${rsvp.allergies}` : ''}
|
||||
📧 Eingegangen am: ${new Date().toLocaleString('de-DE')}
|
||||
🌐 Website: ${siteConfig.domain}
|
||||
|
||||
Diese E-Mail wurde automatisch vom RSVP-System generiert.
|
||||
`;
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||
to: siteConfig.email || process.env.NOTIFICATION_EMAIL,
|
||||
subject: `Neue RSVP: ${rsvp.name} - ${siteConfig.title}`,
|
||||
text: textContent,
|
||||
html: htmlContent,
|
||||
};
|
||||
|
||||
await this.transporter.sendMail(mailOptions);
|
||||
}
|
||||
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
await this.transporter.verify();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('E-Mail-Verbindung fehlgeschlagen:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const emailService = new EmailService();
|
||||
@ -2,12 +2,11 @@ import "dotenv/config";
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
import { siteConfigManager } from "./site-config";
|
||||
|
||||
const app = express();
|
||||
|
||||
// ...existing code...
|
||||
|
||||
// HTTP Basic Authentication middleware
|
||||
// Multi-Site HTTP Basic Authentication middleware
|
||||
const basicAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
// Skip authentication for static assets and development files
|
||||
const skipPaths = [
|
||||
@ -35,12 +34,38 @@ const basicAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
const domain = req.get('host') || 'localhost';
|
||||
const siteConfig = siteConfigManager.getSiteConfig(domain);
|
||||
|
||||
if (!siteConfig) {
|
||||
return res.status(404).send(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Site nicht gefunden</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f9f9f9; }
|
||||
.container { max-width: 400px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||
h1 { color: #e74c3c; margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>❌ Site nicht gefunden</h1>
|
||||
<p>Die Domain <strong>${domain}</strong> ist nicht konfiguriert.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
// For HTML requests (browser navigation), show auth dialog
|
||||
if (req.headers.accept && req.headers.accept.includes('text/html')) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Hochzeit Eillen & Dennis - Nur für eingeladene Gäste"');
|
||||
res.setHeader('WWW-Authenticate', `Basic realm="${siteConfig.auth.realm}"`);
|
||||
return res.status(401).send(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
@ -50,13 +75,13 @@ const basicAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f9f9f9; }
|
||||
.container { max-width: 400px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||
h1 { color: #d4a574; margin-bottom: 20px; }
|
||||
h1 { color: ${siteConfig.theme.primaryColor}; margin-bottom: 20px; }
|
||||
p { color: #666; line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎩 Hochzeit Eillen & Dennis 💒</h1>
|
||||
<h1>🎩 ${siteConfig.title} 💒</h1>
|
||||
<p>Diese Seite ist nur für eingeladene Gäste zugänglich.</p>
|
||||
<p>Bitte verwenden Sie die Zugangsdaten von Ihrer Einladungskarte.</p>
|
||||
</div>
|
||||
@ -72,19 +97,12 @@ const basicAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
|
||||
const [username, password] = credentials.split(':');
|
||||
|
||||
// Simple credentials for wedding guests
|
||||
const validCredentials = [
|
||||
{ username: 'hochzeit', password: 'eillen2026' },
|
||||
{ username: 'gast', password: 'dennis2026' },
|
||||
{ username: 'einladung', password: 'gutsonenberg' }
|
||||
];
|
||||
|
||||
const isValid = validCredentials.some(cred =>
|
||||
const isValid = siteConfig.auth.credentials.some(cred =>
|
||||
cred.username === username && cred.password === password
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Hochzeit Eillen & Dennis - Nur für eingeladene Gäste"');
|
||||
res.setHeader('WWW-Authenticate', `Basic realm="${siteConfig.auth.realm}"`);
|
||||
if (req.headers.accept && req.headers.accept.includes('text/html')) {
|
||||
return res.status(401).send(`
|
||||
<!DOCTYPE html>
|
||||
@ -95,14 +113,14 @@ const basicAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f9f9f9; }
|
||||
.container { max-width: 400px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||
h1 { color: #d4a574; margin-bottom: 20px; }
|
||||
h1 { color: ${siteConfig.theme.primaryColor}; margin-bottom: 20px; }
|
||||
p { color: #666; line-height: 1.6; }
|
||||
.error { color: #e74c3c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎩 Hochzeit Eillen & Dennis 💒</h1>
|
||||
<h1>🎩 ${siteConfig.title} 💒</h1>
|
||||
<p class="error">Ungültige Anmeldedaten</p>
|
||||
<p>Bitte überprüfen Sie Ihre Zugangsdaten von der Einladungskarte.</p>
|
||||
</div>
|
||||
|
||||
@ -1,16 +1,35 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { insertRsvpSchema } from "@shared/schema";
|
||||
import { insertRsvpSchema } from "../shared/schema";
|
||||
import { siteConfigManager } from "./site-config";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// RSVP endpoint
|
||||
// RSVP endpoint mit Multi-Site Support
|
||||
app.post("/api/rsvp", async (req, res) => {
|
||||
try {
|
||||
const domain = req.get('host') || 'localhost';
|
||||
const siteConfig = siteConfigManager.getSiteConfig(domain);
|
||||
|
||||
if (!siteConfig) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Site ${domain} nicht gefunden`
|
||||
});
|
||||
}
|
||||
|
||||
if (!siteConfig.content.rsvpEnabled) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: "RSVP ist für diese Site deaktiviert"
|
||||
});
|
||||
}
|
||||
|
||||
const validatedData = insertRsvpSchema.parse(req.body);
|
||||
const rsvp = await storage.createRsvp(validatedData);
|
||||
res.json({ success: true, rsvp });
|
||||
const result = await storage.createRsvp(validatedData, domain);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
@ -19,6 +38,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
errors: error.errors
|
||||
});
|
||||
} else {
|
||||
console.error('RSVP Fehler:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Serverfehler"
|
||||
@ -27,11 +47,28 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Get all RSVPs (optional, for admin purposes)
|
||||
app.get("/api/rsvps", async (req, res) => {
|
||||
// Site-Konfiguration für Frontend
|
||||
app.get("/api/config", async (req, res) => {
|
||||
try {
|
||||
const rsvps = await storage.getRsvps();
|
||||
res.json(rsvps);
|
||||
const domain = req.get('host') || 'localhost';
|
||||
const siteConfig = siteConfigManager.getSiteConfig(domain);
|
||||
|
||||
if (!siteConfig) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Site ${domain} nicht gefunden`
|
||||
});
|
||||
}
|
||||
|
||||
// Nur öffentliche Konfigurationsdaten senden (ohne Auth-Daten)
|
||||
const publicConfig = {
|
||||
title: siteConfig.title,
|
||||
subtitle: siteConfig.subtitle,
|
||||
theme: siteConfig.theme,
|
||||
content: siteConfig.content,
|
||||
};
|
||||
|
||||
res.json(publicConfig);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
@ -40,6 +77,24 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Admin endpoint für Site-Liste (nur mit gültiger Auth)
|
||||
app.get("/api/sites", async (req, res) => {
|
||||
try {
|
||||
const sites = siteConfigManager.getAllSites();
|
||||
res.json({ sites });
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Serverfehler"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Kompatibilität: Get all RSVPs (gibt leeres Array zurück)
|
||||
app.get("/api/rsvps", async (req, res) => {
|
||||
res.json([]);
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
105
server/site-config.ts
Normal file
105
server/site-config.ts
Normal file
@ -0,0 +1,105 @@
|
||||
export interface SiteConfig {
|
||||
domain: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
email: string;
|
||||
theme: {
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
backgroundColor: string;
|
||||
};
|
||||
auth: {
|
||||
credentials: Array<{
|
||||
username: string;
|
||||
password: string;
|
||||
}>;
|
||||
realm: string;
|
||||
};
|
||||
content: {
|
||||
welcomeMessage: string;
|
||||
rsvpEnabled: boolean;
|
||||
eventDate?: string;
|
||||
eventLocation?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class SiteConfigManager {
|
||||
private configs: Map<string, SiteConfig> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.loadConfigurations();
|
||||
}
|
||||
|
||||
private loadConfigurations() {
|
||||
// Lade Konfigurationen aus Umgebungsvariablen oder JSON-Dateien
|
||||
const configsJson = process.env.SITES_CONFIG;
|
||||
if (configsJson) {
|
||||
try {
|
||||
const configs = JSON.parse(configsJson);
|
||||
for (const [domain, config] of Object.entries(configs)) {
|
||||
this.configs.set(domain, config as SiteConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Site-Konfigurationen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback-Konfiguration für die aktuelle Site
|
||||
if (this.configs.size === 0) {
|
||||
this.configs.set('ed.pixelbrew.de', {
|
||||
domain: 'ed.pixelbrew.de',
|
||||
title: 'Hochzeit Eillen & Dennis',
|
||||
subtitle: 'Gutshof Sonenberg • 2026',
|
||||
email: process.env.NOTIFICATION_EMAIL || 'hochzeit@pixelbrew.de',
|
||||
theme: {
|
||||
primaryColor: '#d4a574',
|
||||
secondaryColor: '#f4e4bc',
|
||||
backgroundColor: '#f9f9f9'
|
||||
},
|
||||
auth: {
|
||||
credentials: [
|
||||
{ username: 'hochzeit', password: 'eillen2026' },
|
||||
{ username: 'gast', password: 'dennis2026' },
|
||||
{ username: 'einladung', password: 'gutsonenberg' }
|
||||
],
|
||||
realm: 'Hochzeit Eillen & Dennis - Nur für eingeladene Gäste'
|
||||
},
|
||||
content: {
|
||||
welcomeMessage: 'Willkommen zur Hochzeit von Eillen & Dennis',
|
||||
rsvpEnabled: true,
|
||||
eventDate: '2026',
|
||||
eventLocation: 'Gutshof Sonenberg'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSiteConfig(domain: string): SiteConfig | null {
|
||||
// Normalisiere Domain (entferne www., Protokoll etc.)
|
||||
const normalizedDomain = this.normalizeDomain(domain);
|
||||
return this.configs.get(normalizedDomain) || null;
|
||||
}
|
||||
|
||||
getAllSites(): string[] {
|
||||
return Array.from(this.configs.keys());
|
||||
}
|
||||
|
||||
private normalizeDomain(domain: string): string {
|
||||
return domain
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/^www\./, '')
|
||||
.split(':')[0]
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
// Für zukünftige dynamische Konfiguration
|
||||
addSiteConfig(domain: string, config: SiteConfig) {
|
||||
this.configs.set(this.normalizeDomain(domain), config);
|
||||
}
|
||||
|
||||
removeSiteConfig(domain: string) {
|
||||
this.configs.delete(this.normalizeDomain(domain));
|
||||
}
|
||||
}
|
||||
|
||||
export const siteConfigManager = new SiteConfigManager();
|
||||
@ -1,45 +1,39 @@
|
||||
import { users, rsvps, type User, type InsertUser, type Rsvp, type InsertRsvp } from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { type InsertRsvp } from "../shared/schema";
|
||||
import { emailService } from "./email";
|
||||
import { siteConfigManager } from "./site-config";
|
||||
|
||||
export interface IStorage {
|
||||
getUser(id: number): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
createRsvp(rsvp: InsertRsvp): Promise<Rsvp>;
|
||||
getRsvps(): Promise<Rsvp[]>;
|
||||
createRsvp(rsvp: InsertRsvp, domain: string): Promise<{ success: boolean; message: string }>;
|
||||
getRsvps(): Promise<any[]>; // Für Kompatibilität, aber wird nicht mehr verwendet
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
async getUser(id: number): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||
return user || undefined;
|
||||
export class EmailStorage implements IStorage {
|
||||
async createRsvp(rsvp: InsertRsvp, domain: string): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const siteConfig = siteConfigManager.getSiteConfig(domain);
|
||||
if (!siteConfig) {
|
||||
throw new Error(`Keine Konfiguration für Domain ${domain} gefunden`);
|
||||
}
|
||||
|
||||
await emailService.sendRsvpNotification(rsvp, siteConfig);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'RSVP erfolgreich versendet'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Versenden der RSVP:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Fehler beim Versenden der RSVP'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async createUser(insertUser: InsertUser): Promise<User> {
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values(insertUser)
|
||||
.returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
async createRsvp(insertRsvp: InsertRsvp): Promise<Rsvp> {
|
||||
const [rsvp] = await db
|
||||
.insert(rsvps)
|
||||
.values(insertRsvp)
|
||||
.returning();
|
||||
return rsvp;
|
||||
}
|
||||
|
||||
async getRsvps(): Promise<Rsvp[]> {
|
||||
return await db.select().from(rsvps);
|
||||
async getRsvps(): Promise<any[]> {
|
||||
// RSVPs werden nicht mehr gespeichert, sondern nur per E-Mail versendet
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
export const storage = new EmailStorage();
|
||||
|
||||
@ -1,35 +1,19 @@
|
||||
import { pgTable, text, serial, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const rsvps = pgTable("rsvps", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
attendance: text("attendance").notNull(), // 'ja', 'nein', 'vielleicht'
|
||||
menu: text("menu"), // 'fleisch', 'vegetarisch', 'vegan'
|
||||
allergies: text("allergies"),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const insertRsvpSchema = createInsertSchema(rsvps).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
// RSVP Schema ohne Datenbank-spezifische Felder
|
||||
export const insertRsvpSchema = z.object({
|
||||
name: z.string().min(1, "Name ist erforderlich"),
|
||||
attendance: z.enum(["ja", "nein", "vielleicht"], {
|
||||
required_error: "Bitte wählt eure Teilnahme aus",
|
||||
}),
|
||||
menu: z.enum(["fleisch", "vegetarisch", "vegan"]).optional(),
|
||||
allergies: z.string().optional(),
|
||||
});
|
||||
|
||||
export type InsertRsvp = z.infer<typeof insertRsvpSchema>;
|
||||
export type Rsvp = typeof rsvps.$inferSelect;
|
||||
|
||||
// Keep existing users table for compatibility
|
||||
export const users = pgTable("users", {
|
||||
id: serial("id").primaryKey(),
|
||||
username: text("username").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
});
|
||||
|
||||
export const insertUserSchema = createInsertSchema(users).pick({
|
||||
username: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
export type User = typeof users.$inferSelect;
|
||||
// Für Kompatibilität mit bestehenden Frontend-Code
|
||||
export type Rsvp = InsertRsvp & {
|
||||
id?: number;
|
||||
createdAt?: Date;
|
||||
};
|
||||
|
||||
17
tsconfig.server.json
Normal file
17
tsconfig.server.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"include": ["server/**/*", "shared/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "vite.config.ts"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
18
vercel.json
18
vercel.json
@ -1,18 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "api/index.ts",
|
||||
"use": "@vercel/node",
|
||||
"config": {
|
||||
"includeFiles": ["package.json"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "api/index.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user