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

617 lines
27 KiB
Markdown
Raw Normal View History

# 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