diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..4a483d84 --- /dev/null +++ b/.env.example @@ -0,0 +1,51 @@ +# OpenCRM – zentrale Konfiguration +# ================================== +# Kopiere diese Datei zu .env und passe die Werte an. +# Diese .env wird sowohl vom Backend (npm run dev) als auch von Docker +# Compose verwendet. + +# ============== PORTS (extern erreichbar auf dem Host) ============== +OPENCRM_PORT=3010 # Backend + Frontend (alles unter einer URL) +ADMINER_PORT=8090 # Adminer (Datenbank-UI). 8081 ist häufig schon belegt. +DB_PORT=3306 # MariaDB extern (für lokale Tools/Dev). 0 = nicht freigeben. + +# ============== DATEN-PFADE (Bind-Mounts) ============== +# Relativ zum Projektverzeichnis. Werden zur Laufzeit angelegt. +DATA_DIR=./data +DB_DATA_DIR=./data/db +UPLOADS_DIR=./data/uploads +FACTORY_DEFAULTS_DIR=./data/factory-defaults +BACKUPS_DIR=./data/backups + +# ============== DATENBANK ============== +DB_NAME=opencrm +DB_USER=opencrm +DB_PASSWORD=change-this-password +DB_ROOT_PASSWORD=change-this-root-password + +# Connection-String fürs Backend (im Container = Service-Name `db`) +# Im Dev-Modus (npm run dev) lokal: localhost:DB_PORT +DATABASE_URL=mysql://root:change-this-root-password@localhost:3306/opencrm + +# ============== SECURITY ============== +# JWT-Secret: min. 32 Zeichen. Generieren: openssl rand -hex 64 +JWT_SECRET=change-this-to-a-very-long-random-secret-please-rotate-before-production +JWT_EXPIRES_IN=7d + +# Encryption-Key für Portal-Credentials: GENAU 64 Hex-Zeichen. +# Generieren: openssl rand -hex 32 +ENCRYPTION_KEY=change-this-to-64-hex-characters-please-rotate-before-production-xx + +# Server +NODE_ENV=development +PORT=3001 # Backend-internal Port (Dev: localhost:3001) +LISTEN_ADDR=0.0.0.0 # In Docker = 0.0.0.0, in Bare-Metal-Production = 127.0.0.1 + +# CORS – nur in Production setzen, wenn Frontend auf separater Domain läuft. +# Beispiel: CORS_ORIGINS=https://crm.deine-domain.de +# CORS_ORIGINS= + +# ============== ADMINER (DB-UI) ============== +# Theme-Auswahl (siehe https://www.adminer.org/#extras) +# Optionen: default, brade, designs/galkaev, pepa-linha-dark, hever, lucas-sandery +ADMINER_DESIGN=pepa-linha-dark diff --git a/.gitignore b/.gitignore index ff9a1cbe..4f761367 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,13 @@ npm-debug.log* tmp/ *.tmp *.bak + +# Docker-Bind-Mounts: Inhalt nicht tracken, Verzeichnisstruktur via .gitkeep behalten +data/db/* +!data/db/.gitkeep +data/uploads/* +!data/uploads/.gitkeep +data/factory-defaults/* +!data/factory-defaults/.gitkeep +data/backups/* +!data/backups/.gitkeep diff --git a/README.md b/README.md index f0732e59..0a0ed5ee 100644 --- a/README.md +++ b/README.md @@ -47,32 +47,62 @@ Web-basiertes CRM-System für Kundenverwaltung mit Verträgen (Energie, Telekomm > - Express 4.x → `@types/express@^4.17.x` > - Express 5.x → `@types/express@^5.x` (erst bei offiziellem Release empfohlen) +## Quick-Start mit Docker (empfohlen) + +Komplettes Setup mit MariaDB + OpenCRM + Adminer (DB-UI) in 3 Befehlen: + +```bash +git clone +cd opencrm +cp .env.example .env # Werte anpassen, Secrets rotieren! +docker compose up -d +``` + +Browser: +- **CRM**: http://localhost:3010 (Login: `admin@admin.com` / `admin`) +- **Datenbank-UI** (Adminer): http://localhost:8081 (Server: `db`, User: `root`, DB: `opencrm`) + +Alle persistenten Daten liegen in `./data/`: + +| Pfad | Inhalt | +|------|--------| +| `./data/db/` | MariaDB-Datafiles | +| `./data/uploads/` | User-Uploads (PDFs, Bilder) | +| `./data/factory-defaults/` | Stammdaten-Kataloge | +| `./data/backups/` | DB-Backups (`npm run db:backup`) | + +Ports + Pfade konfigurierst du in `./.env` (Default-Werte siehe `.env.example`). + +> **Erste Inbetriebnahme:** In der `.env` einmalig `RUN_SEED=true` setzen, +> `docker compose up -d` ausführen, dann wieder auf `false`. Danach existiert +> der initiale Admin-User `admin@admin.com` / `admin`. + ## Voraussetzungen -- Node.js 18+ (empfohlen: 20+) -- Docker & Docker Compose -- npm +- Docker & Docker Compose v2 +- Für Backend-Entwicklung außerhalb von Docker: Node.js 20+ und npm -## Installation +## Installation für Entwicklung (ohne Container) ### 1. Repository klonen ```bash git clone cd opencrm +cp .env.example .env # Konfiguration anpassen ``` -### 2. MariaDB-Datenbank starten +### 2. MariaDB-Container starten ```bash -docker-compose up -d +docker compose up -d db ``` -Dies startet einen MariaDB-Container mit: -- **Port:** 3306 -- **Datenbank:** opencrm -- **Root-Passwort:** rootpassword -- **Benutzer:** opencrm / opencrm123 +Das startet nur die Datenbank (mit Daten in `./data/db/`). +Konfiguration kommt aus `./.env`: + +- **Port:** wie in `DB_PORT` (Standard: 3306, intern auf 127.0.0.1) +- **Datenbank/User/Passwort:** wie in `DB_*`-Variablen Warte ca. 10 Sekunden bis die Datenbank bereit ist. diff --git a/backend/.env.example b/backend/.env.example index d2439622..631e3503 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,3 +1,9 @@ +# Backend nutzt seit v1.1 die zentrale Root-.env im Projektverzeichnis. +# → siehe ../.env.example für alle Variablen +# +# Diese Datei bleibt als Legacy-Fallback: wenn /.env nicht existiert, +# liest das Backend backend/.env (z.B. für isolierte Backend-Tests). + # Database DATABASE_URL="mysql://user:password@localhost:3306/opencrm" diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..1ffec09f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,54 @@ +# Multi-Stage Build: Frontend bauen, dann Backend bauen, dann schlankes Runtime-Image +# --------------------------------------------------------------------------------- + +# ============== STAGE 1: Frontend bauen ============== +FROM node:20-alpine AS frontend-builder +WORKDIR /build/frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --no-audit --no-fund --prefer-offline +COPY frontend/ ./ +RUN npm run build +# Output: /build/frontend/dist/ + +# ============== STAGE 2: Backend bauen (TS → JS) ============== +FROM node:20-alpine AS backend-builder +WORKDIR /build/backend +COPY backend/package.json backend/package-lock.json ./ +RUN npm ci --no-audit --no-fund --prefer-offline +COPY backend/prisma ./prisma +RUN npx prisma generate +COPY backend/tsconfig.json ./ +COPY backend/src ./src +RUN npx tsc +# Output: /build/backend/dist/ + +# ============== STAGE 3: Runtime ============== +FROM node:20-alpine +WORKDIR /app + +# Nur Production-Dependencies + Prisma-Client +COPY backend/package.json backend/package-lock.json ./ +RUN npm ci --omit=dev --no-audit --no-fund --prefer-offline && npm cache clean --force + +# Build-Artefakte aus Stage 2 +COPY --from=backend-builder /build/backend/dist ./dist +COPY --from=backend-builder /build/backend/node_modules/.prisma ./node_modules/.prisma +COPY --from=backend-builder /build/backend/node_modules/@prisma ./node_modules/@prisma +COPY backend/prisma ./prisma + +# Frontend-Build ins public/-Verzeichnis (wird in production-Mode statisch ausgeliefert) +COPY --from=frontend-builder /build/frontend/dist ./public + +# Daten-Verzeichnisse (werden via Bind-Mount überlagert; hier nur als Fallback) +RUN mkdir -p uploads factory-defaults prisma/backups + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget --quiet --tries=1 --spider "http://localhost:${PORT:-3001}/api/health" || exit 1 + +# Beim Start: prisma db push (idempotent), dann node +COPY backend/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["node", "dist/index.js"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100755 index 00000000..0d221dd7 --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Beim Container-Start: Schema in DB pushen (idempotent) + (optional) seed. +# RUN_SEED=true beim ersten Start setzen, danach wieder auf false. +set -e + +echo "[entrypoint] Warte auf Datenbank…" +# Prisma versucht selbst Connect; einfacher Retry-Loop um Race-Bedingungen +# beim parallelen Container-Start abzufangen. +TRIES=30 +until npx prisma db push --skip-generate --accept-data-loss 2>/dev/null; do + TRIES=$((TRIES - 1)) + if [ "$TRIES" -le 0 ]; then + echo "[entrypoint] DB nicht erreichbar – Abbruch" + exit 1 + fi + echo "[entrypoint] DB noch nicht bereit – retry in 2s ($TRIES Versuche übrig)" + sleep 2 +done +echo "[entrypoint] DB-Schema synced" + +if [ "${RUN_SEED:-false}" = "true" ]; then + echo "[entrypoint] RUN_SEED=true – seede DB" + npx prisma db seed || echo "[entrypoint] Seed fehlgeschlagen oder schon gelaufen – ignoriert" +fi + +echo "[entrypoint] Starte Backend…" +exec "$@" diff --git a/backend/src/index.ts b/backend/src/index.ts index 3af97f01..b37975d9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,6 +4,16 @@ import helmet from 'helmet'; import path from 'path'; import dotenv from 'dotenv'; +// .env-Dateien laden – Root-.env hat Priorität (zentrale Konfiguration für +// Dev + Docker), backend/.env als Legacy-Fallback. Im Container sind +// Variablen schon via env_file/environment gesetzt – dotenv überschreibt +// existierende process.env-Werte nicht. +// __dirname zeigt auf src/ (dev via tsx) oder dist/ (build). In beiden Fällen +// liegt Root /.env zwei Ebenen darüber. +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // Root /.env +dotenv.config({ path: path.resolve(__dirname, '../.env') }); // backend/.env (Fallback) +dotenv.config(); // cwd-relativ (Container etc.) + import authRoutes from './routes/auth.routes.js'; import customerRoutes from './routes/customer.routes.js'; import addressRoutes from './routes/address.routes.js'; @@ -43,8 +53,6 @@ import { auditContextMiddleware } from './middleware/auditContext.js'; import { auditMiddleware } from './middleware/audit.js'; import { authenticate } from './middleware/auth.js'; -dotenv.config(); - // ==================== SECURITY: Pflicht-Umgebungsvariablen prüfen ==================== if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { console.error('❌ JWT_SECRET ist nicht gesetzt oder zu kurz (min. 32 Zeichen)'); diff --git a/data/backups/.gitkeep b/data/backups/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/db/.gitkeep b/data/db/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/factory-defaults/.gitkeep b/data/factory-defaults/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/uploads/.gitkeep b/data/uploads/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docker-compose.yml b/docker-compose.yml index eef02faa..5c74da63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,13 @@ -version: '3.8' +# OpenCRM – komplettes Setup: MariaDB + Backend/Frontend + Adminer +# Konfiguration über ./.env (siehe ./.env.example) +# +# Quick-Start: +# cp .env.example .env # Werte anpassen (Secrets rotieren!) +# docker compose up -d # erstes Mal: holt Images, baut Backend, startet alles +# open http://localhost:${OPENCRM_PORT} # CRM +# open http://localhost:${ADMINER_PORT} # DB-UI +# +# Daten liegen alle unter ./data/* – Bind-Mounts statt Volumes (auf Wunsch). services: db: @@ -6,20 +15,59 @@ services: container_name: opencrm-db restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_DATABASE: opencrm - MYSQL_USER: opencrm - MYSQL_PASSWORD: opencrm123 + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MARIADB_DATABASE: ${DB_NAME} + MARIADB_USER: ${DB_USER} + MARIADB_PASSWORD: ${DB_PASSWORD} ports: - - "3306:3306" + # Externe Erreichbarkeit für lokale DB-Tools (TablePlus etc.). + # Auf 127.0.0.1 binden – kein public exposure. + - "127.0.0.1:${DB_PORT:-3306}:3306" volumes: - - mariadb_data:/var/lib/mysql + - ${DB_DATA_DIR:-./data/db}:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - start_period: 10s + start_period: 20s interval: 10s timeout: 5s - retries: 3 + retries: 5 -volumes: - mariadb_data: + opencrm: + build: + context: . + dockerfile: backend/Dockerfile + container_name: opencrm-app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + # Connection ins Container-Netzwerk (Service-Name = Hostname) + DATABASE_URL: "mysql://root:${DB_ROOT_PASSWORD}@db:3306/${DB_NAME}" + JWT_SECRET: ${JWT_SECRET} + JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d} + ENCRYPTION_KEY: ${ENCRYPTION_KEY} + NODE_ENV: production + PORT: 3001 + LISTEN_ADDR: 0.0.0.0 + CORS_ORIGINS: ${CORS_ORIGINS:-} + RUN_SEED: ${RUN_SEED:-false} + ports: + - "${OPENCRM_PORT:-3010}:3001" + volumes: + # Bind-Mounts für persistente Daten unter ./data/ + - ${UPLOADS_DIR:-./data/uploads}:/app/uploads + - ${FACTORY_DEFAULTS_DIR:-./data/factory-defaults}:/app/factory-defaults + - ${BACKUPS_DIR:-./data/backups}:/app/prisma/backups + + adminer: + image: adminer:latest + container_name: opencrm-adminer + restart: unless-stopped + depends_on: + - db + environment: + ADMINER_DEFAULT_SERVER: db + ADMINER_DESIGN: ${ADMINER_DESIGN:-pepa-linha-dark} + ports: + - "127.0.0.1:${ADMINER_PORT:-8081}:8080"