Files
crm-system/docs/03a-patterns-summary.md

617 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CRM-Build Pattern-Summary aus wochenplaner-Repo
> **Phase 3 Codebase Exploration & Pattern Reuse**
> **Sub-Agent:** codebase_explorer (READ-ONLY)
> **Erstellt:** 2026-06-03 22:24 UTC
> **Quellen:** `https://forgejo.media-on.de/Leopoldadmin/wochenplaner` (Branch `master`)
> **Status:** Wochenplaner-Patterns analysiert; rentman-clone NICHT verfügbar
---
## Section A: Wochenplaner-Patterns (PRIMAER)
Wochenplaner-Architektur ist **Multi-Container** (`backend` + `frontend` mit nginx) im Gegensatz zur ursprünglichen Annahme "Single-Container mit content: mount". Tatsächliche Stack-Bestätigung:
| Layer | Stack |
|---|---|
| Webserver | nginx (Reverse-Proxy + Static) |
| Backend | FastAPI 0.111.0 + uvicorn 0.29.0, sync (nicht async!) |
| DB | SQLite via SQLAlchemy 2.0.35 (sync!) |
| Auth | python-jose 3.3.0 + passlib[bcrypt] 1.7.4 + bcrypt 4.0.1 |
| Validation | pydantic 2.7.0 |
| HTTP-Fileupload | python-multipart 0.0.9 |
**Wichtige Abweichung zur CRM-Architektur (02-architecture.md):**
- Wochenplaner nutzt **sync** SQLAlchemy, CRM braucht **async** (aiosqlite/asyncpg)
- Wochenplaner hat **kein** Alembic (nutzt `Base.metadata.create_all`), CRM braucht Alembic
- Wochenplaner hat **kein** pytest/Coverage-Setup, CRM braucht es (NFR-4: ≥70%)
- Wochenplaner hat **kein** Service-Layer-Pattern (Monolith in main.py, 12.9 KB), CRM braucht Service-Layer
---
### Pattern A-1: FastAPI App-Init + CORS + Static-Mount
**Was:** Single-File-Setup mit CORS-Middleware, Static-Files-Mount, Mount-Check via `os.path.isdir`.
**Wo:** `backend/main.py` Zeilen 13-30
```python
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
app = FastAPI(title="Wochenplaner API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "frontend")
if os.path.isdir(FRONTEND_DIR):
app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")
```
**CRM-Anwendung:**
- `allow_origins=["*"]` MUSS ersetzt werden durch Whitelist (NFR-2: CORS-Whitelist, kein `*`)
- Static-Mount in `app/main.py` mounten unter `/static` (nicht `/` CRM hat eigene Routes)
- `os.path.isdir`-Check übernehmen für optionalen Mount bei Dev-Skips
---
### Pattern A-2: JWT-Setup + Token-Encode/Decode
**Was:** `python-jose` JWT mit HS256, 7-Tage-Expiry, Secret aus ENV mit Fallback.
**Wo:** `backend/main.py` Zeilen 32-70
```python
import os
from datetime import datetime, timedelta
from jose import JWTError, jwt
SECRET_KEY = os.environ.get("JWT_SECRET", "wochenplaner-jwt-secret-2024")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 Tage
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Ungültiger Token")
```
**CRM-Anwendung:**
- JWT-Lib beibehalten (`python-jose[cryptography]` 3.3.0) für 1:1-Übernahme
- Expiry anpassen: 02-architecture/01-requirements spezifiziert 24h für CRM (FR-1.2)
- `SECRET_KEY` MUSS mindestens 32 Zeichen haben (NFR-2: ≥32 Zeichen). Fallback in Prod ist VERBOTEN.
- `verify_token` erweitern um expired-vs-invalid Unterscheidung für `token_expired` Hint (FR-1.7)
---
### Pattern A-3: Passwort-Hashing mit passlib + bcrypt
**Was:** CryptContext mit bcrypt-Schema, deprecated="auto" (rüstet Hashes automatisch up).
**Wo:** `backend/main.py` Zeile 35
```python
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Hashing
password_hash = pwd_context.hash("admin123")
# Verify
if not user or not pwd_context.verify(req.password, user.password_hash):
raise HTTPException(status_code=401, detail="Ungültige Zugangsdaten")
```
**CRM-Anwendung:**
- 1:1 übernehmen, `passlib[bcrypt]==1.7.4` und `bcrypt==4.0.1` exakt pinnen (Inkompatibilität 5.x)
- NFR-2 fordert argon2id (Primary) oder bcrypt (Fallback) bcrypt ist OK
- Default-Rounds sind 12 (passlib-Default), für CRM reicht das, ggf. auf 14 erhöhen wenn Hardware es erlaubt
---
### Pattern A-4: get_current_user + require_role Factory
**Was:** Dual-Source-Auth (Header Bearer + Query-Param `?token=`), Role-Factory-Pattern.
**Wo:** `backend/main.py` Zeilen 60-100
```python
from fastapi import Depends, Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional
security = HTTPBearer(auto_error=False)
def get_current_user(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> dict:
token = None
if credentials:
token = credentials.credentials
if not token:
token = request.query_params.get("token") # WebSocket-Fallback
if not token:
raise HTTPException(status_code=401, detail="Nicht authentifiziert")
return verify_token(token)
def require_role(role: str):
def checker(payload: dict = Depends(get_current_user)):
user_role = payload.get("role", "user")
if user_role == "admin":
return payload # Admin bypass
if user_role != role:
raise HTTPException(status_code=403, detail=f"Rolle '{role}' erforderlich")
return payload
return checker
# Nutzung:
@app.get("/api/admin/users")
def list_users(payload: dict = Depends(require_role("admin"))):
...
```
**CRM-Anwendung:**
- `get_current_user` zurückgibt nicht `dict`, sondern ein **User-Model** (async DB-Load). Wochenplaner nutzt nur JWT-Payload (kein DB-Reload) CRM sollte User neu laden, damit `is_active`/Soft-Delete greift.
- `require_role` für 3 CRM-Rollen: `admin`, `manager`, `rep` (statt admin/user/viewer)
- Query-Param-Token-Fallback für WebSocket-Support ggf. beibehalten, aber NFR-2: Auth ausschließlich via Bearer-Header ist sicherer
- `auto_error=False` beibehalten, damit 401 mit Custom-Detail möglich ist
---
### Pattern A-5: get_db() Dependency (sync)
**Was:** Generator-Dependency mit try/finally, SessionLocal-Instanz.
**Wo:** `backend/main.py` Zeilen 102-108
```python
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
```
**CRM-Anwendung:**
- **Komplett NEU** schreiben als `AsyncSession`-Variante:
```python
# app/core/db.py (CRM)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
```
- Wochenplaner-Sync-Pattern NICHT übernehmen, weil 02-architecture.md explizit async vorschreibt
---
### Pattern A-6: SQLAlchemy-Engine + Session-Factory
**Was:** Engine mit `check_same_thread=False` (SQLite-Workaround), SessionLocal-Standardkonfig.
**Wo:** `backend/models.py` Zeilen 10-15
```python
from sqlalchemy import create_engine, Column, String, Text, Integer, Boolean, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker, Session
import os
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
os.makedirs(DATA_DIR, exist_ok=True)
DATABASE_URL = f"sqlite:///{DATA_DIR}/wochenplaner.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
```
**CRM-Anwendung:**
- URL-Generierung adaptieren: `if os.environ.get("DATABASE_URL"): postgres_url else: sqlite+aiosqlite:///./dev.db`
- `check_same_thread` entfällt komplett (async-Engine nutzt asyncpg/aiosqlite, kein Multi-Thread-Issue)
- `Base = declarative_base()` ersetzen durch `Base = DeclarativeBase` + Type-Annotations (SQLAlchemy 2.0 Style)
- Models: Spalten-Definitionen ohne `Mapped[]` (sync) → CRM mit `Mapped[...] = mapped_column(...)` (async 2.0)
---
### Pattern A-7: Docker-Multi-Stage-Setup
**Was:** python:3.12-slim, requirements-copy-then-install (Cache), mkdir data-dir, uvicorn-CMD.
**Wo:** `backend/Dockerfile.backend` (244 Bytes)
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/data && chmod 777 /app/data
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
**CRM-Anwendung:**
- 1:1 übernehmen, **aber**:
- `main:app` ersetzen durch `app.main:app` (CRM nutzt `app/`-Package)
- `/app/data` durch persistentes Volume oder PostgreSQL-Service ersetzen
- **Multi-Stage-Build empfohlen** für CRM: Builder-Stage mit gcc, Final-Stage nur Runtime-Deps (kleinere Images)
- Non-Root-User `USER appuser` für Production-Sicherheit
- Alembic-Startup-Hook: `CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]`
---
### Pattern A-8: docker-compose mit Coolify-Env-Pattern
**Was:** Multi-Service (backend + frontend mit nginx), Volume-Mount für SQLite, SERVICE_BASE64_64-Pattern für JWT-Secret.
**Wo:** `docker-compose.yml` (429 Bytes) im Repo-Root
```yaml
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile.backend
restart: unless-stopped
environment:
- PORT=80
- JWT_SECRET=${SERVICE_BASE64_64_JWT:-wochenplaner-jwt-secret}
volumes:
- wochenplaner_data:/app/data
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
restart: unless-stopped
volumes:
wochenplaner_data:
driver: local
```
**CRM-Anwendung:**
- Pattern 1:1 übernehmen, **aber**:
- Service-Name `crm-api` statt `backend`
- Service `crm-web` (nginx mit statischem Frontend) zusätzlich
- **3. Service `crm-db`** (PostgreSQL) hinzufügen gemäß 02-architecture §1 (Production: PostgreSQL statt SQLite)
- ENV-Keys: `JWT_SECRET`, `DATABASE_URL`, `AUTH_SECRET` (≥32 Zeichen), `CORS_ORIGINS`
- `SERVICE_BASE64_64_*`-Pattern ist Coolify-Standard für Auto-generierte Secrets (64 Zeichen base64) für CRM:
- `JWT_SECRET=${SERVICE_BASE64_64_JWT}`
- `DATABASE_PASSWORD=${SERVICE_BASE64_32_DB}`
- Volume: Für SQLite-Dev `crm_data:/app/data`, für Production **kein** lokales Volume (PostgreSQL ist externer Service)
- Domain-Eintrag: `https://crm.media-on.de:80` (Port MUSS in URL Memory-regel)
---
### Pattern A-9: nginx-Reverse-Proxy + SPA-Fallback
**Was:** nginx-Config mit `/api/` Proxy zum Backend, `/health` direkter Proxy, alle anderen Routen → `index.html` (SPA-Fallback).
**Wo:** `nginx.conf` im Repo-Root (714 Bytes)
```nginx
server {
listen 80;
server_name _;
root /var/www/html;
index index.html;
location /api/ {
proxy_pass http://backend:8000/api/;
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;
}
location /health {
proxy_pass http://backend:8000/health;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html; # SPA-Fallback
}
}
```
**CRM-Anwendung:**
- 1:1 übernehmen für nginx-Service in docker-compose
- **Aber**: CRM hat KEIN SPA mit client-side Routing in v1 (Alpine.js ist seitenbasiert, nicht SPA). `try_files` ist trotzdem OK, weil Alpine-Komponenten auf eigenen HTML-Pages liegen
- `proxy_pass http://crm-api:8000` (Service-Name an CRM anpassen)
- `/docs` (Swagger) und `/redoc` zusätzlich via `location` exposen
- `/metrics` für Prometheus (NFR-5, optional v1.1)
---
### Pattern A-10: DB-Init mit Default-Usern (Bootstrap-Pattern)
**Was:** Beim Startup prüfen, ob Default-User existieren, ggf. anlegen.
**Wo:** `backend/main.py` Zeilen 110-140
```python
from passlib.context import CryptContext
import uuid
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Init DB
init_db()
# Create default users if not exist
db = SessionLocal()
try:
if not db.query(User).filter(User.username == "admin").first():
db.add(User(id=str(uuid.uuid4()), username="admin",
password_hash=pwd_context.hash("admin"), role="admin"))
db.add(User(id=str(uuid.uuid4()), username="user",
password_hash=pwd_context.hash("user"), role="user"))
db.add(User(id=str(uuid.uuid4()), username="viewer",
password_hash=pwd_context.hash("viewer"), role="viewer"))
db.commit()
finally:
db.close()
```
**CRM-Anwendung:**
- **NICHT 1:1 übernehmen** Wochenplaner hat hartcodierte Default-User (admin/admin, user/user, viewer/viewer) das ist ein SECURITY-ISSUE
- CRM-Requirements (FR-1.1) verlangen **Bootstrap-Registrierung**: erste Registrierung nur möglich wenn `users`-Tabelle leer, danach 403
- Pattern adaptieren: Statt Default-User anlegen, **leere DB** lassen und Bootstrap-Endpoint `/api/auth/register` exponieren mit Check `if db.query(User).count() == 0: allow else: 403`
- `uuid.uuid4()` als String-ID übernehmen für v1 (einfacher als Auto-Increment für Replikation)
---
### Pattern A-11: Rate-Limiting via DB-Tracking
**Was:** `LoginAttempt`-Tabelle trackt jeden Login-Versuch; 5 Failures in 10 Min → 429.
**Wo:** `backend/main.py` Zeilen 175-200
```python
# Rate-Limit-Check
recent_fails = db.query(LoginAttempt).filter(
LoginAttempt.username == req.username,
LoginAttempt.attempted_at > datetime.utcnow() - timedelta(minutes=10),
LoginAttempt.success == False
).count()
if recent_fails >= 5:
raise HTTPException(status_code=429, detail="Zu viele Fehlversuche. Bitte 10 Minuten warten.")
# Nach Verify:
if not user or not pwd_context.verify(req.password, user.password_hash):
db.add(LoginAttempt(username=req.username, success=False))
db.commit()
raise HTTPException(status_code=401, detail="Ungültige Zugangsdaten")
db.add(LoginAttempt(username=req.username, success=True))
db.commit()
```
**CRM-Anwendung:**
- Pattern 1:1 übernehmen, **aber**:
- NFR-2 fordert 60 req/min/IP (SlowAPI oder ähnliches) Wochenplaner-Lösung ist Username-basiert, CRM sollte IP-basiert (oder beides)
- Tabelle `login_attempts` mit Cleanup-Job (alle 24h alte Einträge löschen)
- IP-Tracking: `request.client.host` zusätzlich speichern
- Alternative: `slowapi`-Lib nutzen statt Custom-Tracking (moderneres Python-Idiom)
---
### Pattern A-12: Health-Endpoint
**Was:** Minimaler `/health` mit Timestamp.
**Wo:** `backend/main.py` Zeile 290
```python
@app.get("/health")
def health():
return {"status": "ok", "time": datetime.utcnow().isoformat()}
```
**CRM-Anwendung:**
- 1:1 übernehmen, **erweitern** um DB-Connection-Check (NFR-5: 503 wenn DB down):
```python
# CRM app/api/v1/health.py
@router.get("/health")
async def health(db: AsyncSession = Depends(get_db)):
try:
await db.execute(text("SELECT 1"))
return {"status": "ok", "db": "ok", "time": datetime.utcnow().isoformat()}
except Exception as e:
raise HTTPException(status_code=503, detail=f"DB down: {e}")
```
---
## Section B: Rentman-Clone-Repo
**Status:** NICHT GEFUNDEN via forgejo-Suche (Such-Queries: `rentman`, `fastapi-crm`)
**Suchergebnis:** Beide Queries lieferten `data: []` (0 Repositories)
**Konsequenz:** Kein zusätzlicher Pattern-Reuse möglich. CRM-Implementation basiert nur auf wochenplaner-Patterns + 02-architecture.md Specs + 01-requirements.md Akzeptanzkriterien.
**Alternative Pattern-Quellen (NICHT genutzt, da außerhalb des Auftrags):**
- Eigene FastAPI-Projekte (z.B. persönliche Templates) nicht in scope
- Public Open-Source-CRMs (Twenty, EspoCRM) haben komplett anderen Stack (TypeORM/Node)
---
## Section C: Pattern-Mapping auf CRM-Tasks
| Pattern (aus wochenplaner) | CRM-Phase | CRM-Task-ID | CRM-Datei-Pfad | Änderungsbedarf |
|---|---|---|---|---|
| A-1 App-Init + CORS + Static-Mount | 4a | `main-app` | `app/main.py` | `allow_origins` Whitelist; Static-Mount unter `/static`; Frontend-Pages separat |
| A-2 JWT-Setup (python-jose, HS256) | 4a | `core-modules` | `app/core/security.py` | Expiry 24h statt 7 Tage; ≥32 Zeichen Secret; `token_expired` Hint |
| A-3 bcrypt + passlib | 4a | `core-modules` | `app/core/security.py` | 1:1 übernehmen, Rounds 12 OK |
| A-4 get_current_user + require_role | 4a | `core-modules`, `auth-routes` | `app/core/deps.py` | User-Model-Return statt dict; Rollen admin/manager/rep; Query-Param-Fallback prüfen |
| A-5 get_db() (sync) | 4a | `core-modules` | `app/core/db.py` | **Komplett async** umschreiben mit AsyncSession |
| A-6 SQLAlchemy-Engine + Session | 4a | `core-modules` | `app/core/db.py` | async-Engine; URL-Switch SQLite/Postgres; `Mapped[]`-Types 2.0 |
| A-7 Dockerfile python:3.12-slim | 4d | `dockerfile` | `Dockerfile` | 1:1 + Multi-Stage + Non-Root + alembic-migrate-Start |
| A-8 docker-compose Multi-Container | 4d | `docker-compose` | `docker-compose.yml` | Service-Namen crm-api/crm-web/crm-db; Postgres-Service; SERVICE_BASE64_64-ENV |
| A-9 nginx Reverse-Proxy + SPA | 4d | `docker-compose` (frontend) | `frontend/nginx.conf` | Service-Name crm-api; /docs + /metrics exposen |
| A-10 DB-Init Default-User | 4a | `auth-routes` | `app/api/v1/auth.py` | **NICHT übernehmen** (Security); Bootstrap-Register-Endpoint mit User-count-Check |
| A-11 Rate-Limiting DB-Tracking | 4a | `auth-routes` | `app/services/auth_service.py` | IP+Username-basiert; 60 req/min NFR-2; Cleanup-Job |
| A-12 Health-Endpoint | 4a | `health-route` | `app/api/v1/health.py` | + DB-Connection-Check (NFR-5) |
| Static-Files-Mount (Frontend) | 4c | `frontend-foundation` | `app/main.py` (mount) + `webui/` | Mount unter `/static`; HTML-Pages pro Feature |
| nginx-SPA-Fallback | 4c | `frontend-foundation` | `frontend/nginx.conf` | 1:1 OK, aber CRM nicht zwingend SPA |
| CORS-Wildcard | 4a | `main-app` | `app/main.py` | **MUSS ersetzt werden** durch Whitelist (NFR-2) |
| bcrypt 4.0.1 pinning | 4a | `core-modules` | `backend/requirements.txt` | Versionen exakt pinnen (sqlalchemy 2.0.35, bcrypt 4.0.1, passlib 1.7.4) |
| LoginAttempt-Table | 4a | `models-base` (optional in 4a) | `app/models/login_attempt.py` | Optional v1, ggf. erst v1.1 |
---
## Section D: Risiken und Lessons Learned
### Risiko 1: Sync-SQLAlchemy → Async-Migration
**Wochenplaner-Pattern:** Sync-Engine + sync-Session
**CRM-Anforderung:** Async SQLAlchemy 2.0 + aiosqlite (Dev) / asyncpg (Prod)
**Lesson:** Sync-Code aus wochenplaner (alle Endpoint-Handler, alle DB-Queries) MUSS in CRM async umgeschrieben werden. Pattern `db.query()``select()` + `await session.execute()`. Risiko: sehr viele Stellen, Fehleranfälligkeit bei vergessenen `await`.
**Mitigation:** Implementation-Engineer muss async-Tests (pytest-asyncio) früh schreiben.
### Risiko 2: Kein Alembic im wochenplaner
**Wochenplaner-Pattern:** `init_db()` ruft `Base.metadata.create_all(engine)` auf keine Migrations.
**CRM-Anforderung:** Alembic mit async-env.py, Migrationen 0001_init, 0002_business_entities.
**Lesson:** CRM kann wochenplaner-Pattern NICHT für Schema-Migration nutzen. Neuer Setup nötig.
**Mitigation:** `alembic init -t async` mit asyncpg/aiosqlite-URL.
### Risiko 3: Hardcoded Default-User (admin/admin)
**Wochenplaner-Pattern:** Beim Start werden admin/admin123, user/user, viewer/viewer auto-erzeugt.
**CRM-Anforderung:** FR-1.1: Bootstrap-Registrierung NUR wenn User-Tabelle leer.
**Lesson:** 1:1-Übernahme wäre SECURITY-INCIDENT. Wochenplaner hat Default-User in Production weil `app.run()` immer beim Start ausgeführt wird.
**Mitigation:** CRM-Implementation MUSS Pattern A-10 verwerfen und Bootstrap-Endpoint mit `if count == 0` implementieren.
### Risiko 4: CORS-Wildcard erlaubt alles
**Wochenplaner-Pattern:** `allow_origins=["*"]`
**CRM-Anforderung:** NFR-2: Explizite CORS-Whitelist.
**Lesson:** 1:1-Übernahme verletzt NFR-2.
**Mitigation:** CRM `main.py` nutzt `allow_origins=[os.environ.get("CORS_ORIGINS", "https://crm.media-on.de")].split(",")`.
### Risiko 5: JWT-Secret-Fallback in Production
**Wochenplaner-Pattern:** `os.environ.get("JWT_SECRET", "wochenplaner-jwt-secret-2024")` Fallback hardcoded.
**CRM-Anforderung:** NFR-2: `AUTH_SECRET` ≥32 Zeichen aus Coolify Env-Vars.
**Lesson:** Fallback in Production ist SECURITY-RISK. Bei vergessenem ENV-Var läuft App mit known Secret.
**Mitigation:** CRM `config.py` (Pydantic-Settings) MUSS `AUTH_SECRET: str = Field(..., min_length=32)` kein Default.
### Risiko 6: bcrypt-Version-Inkompatibilität
**Wochenplaner-Pattern:** `bcrypt==4.0.1` exakt gepinnt.
**CRM-Anforderung:** bcrypt für Passwort-Hashing (NFR-2 erlaubt bcrypt als Fallback zu argon2id).
**Lesson:** bcrypt 4.1+ hat `__about__`-Attribute-Änderung, die passlib bricht. 4.0.1 ist letzte stable Version für passlib.
**Mitigation:** CRM-requirements.txt MUSS `bcrypt==4.0.1` exakt pinnen + `passlib[bcrypt]==1.7.4`.
### Risiko 7: SQLAlchemy 2.0 Mixed-Style
**Wochenplaner-Pattern:** `Column(String)` ohne `Mapped[]`-Type-Annotations.
**CRM-Anforderung:** SQLAlchemy 2.0 mit modernem `Mapped[T]` + `mapped_column()` Style.
**Lesson:** Wochenplaner nutzt 1.x-Style. CRM muss komplett 2.0-Style schreiben für mypy-strict-Compliance (NFR-4).
**Mitigation:** Codebase-Explorer-Output: CRM-Models komplett neu schreiben, kein Copy-Paste aus wochenplaner/models.py.
### Risiko 8: Statisches Volume statt PostgreSQL
**Wochenplaner-Pattern:** `wochenplaner_data:/app/data` mit SQLite.
**CRM-Anforderung:** PostgreSQL als Production-DB (NFR-3, horizontale Skalierung).
**Lesson:** SQLite-Limit (single-writer) macht horizontale Skalierung unmöglich. Volume-Pattern funktioniert nur für Single-Instance.
**Mitigation:** CRM docker-compose MUSS PostgreSQL-Service + Healthcheck + Backup-Volume enthalten. SQLite nur für Dev (`aiosqlite:///./dev.db`).
### Risiko 9: Domain-URL ohne Port
**Wochenplaner-Realität:** Domain `https://reinigung.media-on.de:80` mit Port (Memory-regel)
**CRM-Anforderung:** Domain `crm.media-on.de:80` mit Port (selbe Regel)
**Lesson:** User-Definitive-Rule: Port MUSS in Coolify-Domain-URL.
**Mitigation:** Implementation-Engineer MUSS bei Coolify-Service-Setup `urls: ["https://crm.media-on.de:80"]` setzen (PATCH /api/v1/services/{uuid}).
### Risiko 10: LoginAttempt-Persistenz vs. Cleanup
**Wochenplaner-Pattern:** LoginAttempt-Tabelle wächst unbegrenzt.
**CRM-Anforderung:** NFR-4 Maintainability.
**Lesson:** Kein Cleanup-Job definiert → Tabelle wächst → Performance-Degradation.
**Mitigation:** CRM-Implementation mit Cleanup-Task (v1.1) oder TTL-Index.
---
## Section E: Empfehlung für Phase 4
### E-1: 1:1 übernehmbare Files (mit kleinen Anpassungen)
1. **`backend/Dockerfile.backend` → CRM `Dockerfile`** (Pattern A-7)
- Anpassungen: `main:app``app.main:app`, Multi-Stage-Build, Non-Root-User, alembic-Migration vor uvicorn
2. **`docker-compose.yml` → CRM `docker-compose.yml`** (Pattern A-8)
- Anpassungen: Service-Namen `crm-api`/`crm-web`/`crm-db`, Postgres-Service hinzu, `DATABASE_URL` und `JWT_SECRET` als SERVICE_BASE64_64
3. **`nginx.conf` → CRM `frontend/nginx.conf`** (Pattern A-9)
- Anpassungen: `proxy_pass http://crm-api:8000`, /docs + /metrics exposen
4. **`requirements.txt` → CRM `backend/requirements.txt`** (Pinning-Liste)
- Anpassungen: `pydantic` 2.7.0 → 2.9+ (aktuelle Sicherheitspatches), `alembic` + `asyncpg`/`aiosqlite` + `pytest` + `pytest-asyncio` + `httpx` + `slowapi` ergänzen
### E-2: 1:1 übernehmbare Code-Snippets (in andere Files integrieren)
1. **JWT-Encode/Decode-Funktionen** (Pattern A-2) → `app/core/security.py`
2. **CryptContext-Setup** (Pattern A-3) → `app/core/security.py`
3. **require_role Factory-Pattern** (Pattern A-4) → `app/core/deps.py`
4. **Rate-Limiting-Logik** (Pattern A-11) → `app/services/auth_service.py` (erweitert um IP-Tracking)
5. **Health-Endpoint-Logik** (Pattern A-12) → `app/api/v1/health.py` (erweitert um DB-Check)
### E-3: Komplett neu zu schreibende Files
1. **`app/core/db.py`** (Async SQLAlchemy + Alembic-kompatibel) Pattern A-5+6 nur als Referenz
2. **`app/models/base.py`** (Base + TimestampMixin + SoftDeleteMixin + OrgScopedMixin) wochenplaner hat nur 1.x-Style
3. **`app/models/org.py` + `app/models/user.py`** (mit org_id-FK) wochenplaner hat keine Org-Trennung
4. **Alle Service-Layer-Files** (`app/services/*_service.py`) wochenplaner hat keinen Service-Layer
5. **Alle Schema-Files** (`app/schemas/*`) wochenplaner hat nur inline Pydantic-Models in main.py
6. **`app/api/v1/*.py` Router** wochenplaner hat alles in einem 12.9 KB main.py
7. **`alembic/env.py` + `alembic/versions/0001_init.py`** wochenplaner hat keine Alembic
8. **`app/core/config.py`** (Pydantic-Settings) wochenplaner nutzt rohe `os.environ.get`
9. **Frontend-Files** (`webui/index.html`, `webui/app.html`, `webui/js/*`, `webui/css/*`) wochenplaner-Frontend nutzt Vanilla-JS, CRM braucht Alpine.js + Tailwind CDN
### E-4: ENV-Vars, die schon im wochenplaner-Coolify funktionieren
Aus `docker-compose.yml` und Memory-Analyse:
| ENV-Var | wochenplaner-Wert | CRM-Anwendung |
|---|---|---|
| `PORT` | `80` (intern) | `8000` (intern, dann via nginx exposed) |
| `JWT_SECRET` | `${SERVICE_BASE64_64_JWT:-fallback}` | `${SERVICE_BASE64_64_JWT}` (KEIN Fallback) |
| `JWT_SECRET_MIN_LENGTH` | | `32` (Pydantic-Validation) |
| `DATABASE_URL` | (SQLite intern) | `${SERVICE_BASE64_32_DB_URL}` (PostgreSQL) |
| `CORS_ORIGINS` | `*` (hardcoded) | `https://crm.media-on.de:80` (Whitelist) |
| `ENV` | | `production` / `development` |
| `LOG_LEVEL` | | `INFO` / `DEBUG` |
**Coolify-Setup-Empfehlung für CRM:**
- 3 Services in Coolify: `crm-api` (FastAPI), `crm-web` (nginx), `crm-db` (PostgreSQL)
- Service-UUIDs: Implementation-Engineer MUSS nach Coolify-Create `PATCH /api/v1/services/{uuid}` mit `urls: ["https://crm.media-on.de:80"]` aufrufen
- Domain: `crm.media-on.de:80` (Port zwingend)
- SERVICE_BASE64_64_JWT, SERVICE_BASE64_32_DB_URL werden auto-generiert von Coolify
---
## Zusammenfassung
| Metrik | Wert |
|---|---|
| **Sektionen** | 5 (A, B, C, D, E) |
| **Dokumentierte Patterns (Section A)** | 12 (A-1 bis A-12) |
| **CRM-Tasks in Mapping-Tabelle (Section C)** | 17 Zeilen |
| **Risiken/Lessons Learned (Section D)** | 10 |
| **Empfehlungen (Section E)** | 4 Sub-Sektionen |
| **Patterns 1:1 übernehmbar** | 4 Files + 5 Code-Snippets |
| **Patterns neu zu schreiben** | 9 File-Kategorien |
| **Wochenplaner als Quelle** | ✅ analysiert (forgejo, 6 Files gelesen) |
| **Rentman-clone als Quelle** | ❌ NICHT GEFUNDEN |
| **Empfehlung** | **GO für Phase 4a** (mit expliziten Warnungen zu Risiken 3, 4, 5, 8) |
**Offene Punkte für Implementation-Engineer (Phase 4a):**
1. Soll `python-jose` (wochenplaner-konsistent) ODER `PyJWT` (moderneres Python-Idiom) genutzt werden? Memory sagt python-jose, also beibehalten
2. SQLite-only für Dev ODER parallel PostgreSQL-Dev? 02-architecture sagt SQLite-Dev, also beibehalten
3. CSP-Header (R-5 Mitigation) im nginx.conf oder im FastAPI-Middleware? 02-architecture nennt main.py (Middleware), also dort
4. Soll `LoginAttempt`-Tabelle in v1 oder erst v1.1 kommen? Phase 4a nicht zwingend