# 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