Umstellung auf E-Mail-System und Multi-Site ohne Datenbank

This commit is contained in:
michi 2025-06-24 14:39:45 +02:00
parent e0181c2b5d
commit 908b4d6ad1
20 changed files with 999 additions and 1761 deletions

View File

@ -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

View File

@ -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
View 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
View 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
View 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"

View 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;
# }

View 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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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
View 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();

View File

@ -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>

View File

@ -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
View 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();

View File

@ -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();

View File

@ -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
View 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"]
}
}

View File

@ -1,18 +0,0 @@
{
"version": 2,
"builds": [
{
"src": "api/index.ts",
"use": "@vercel/node",
"config": {
"includeFiles": ["package.json"]
}
}
],
"routes": [
{
"src": "/(.*)",
"dest": "api/index.ts"
}
]
}