# leocrm — Architektur (v0.1) ## 1. Überblick `leocrm` ist eine monolithische FastAPI-Webanwendung, die HTML-Views (Jinja2) **und** eine JSON-API parallel anbietet. Beide Schichten teilen sich dieselben Service-Layer-Funktionen, dadurch sind Headless-Tests ohne Browser möglich. ``` ┌─────────────────────────────────────────────────┐ │ Browser (HTML/Jinja2) │ │ ↓ 302-Login-Redirect bei fehlender Session │ ├─────────────────────────────────────────────────┤ │ FastAPI App (app/main.py) │ │ ├── / /login /logout /companies/* │ │ │ /contacts/* → HTML-Routes (Jinja2) │ │ │ │ │ └── /api/health /api/auth/* /api/companies/* │ │ /api/contacts/* → JSON-Routes │ ├─────────────────────────────────────────────────┤ │ Service Layer (app/services/) │ │ ├── auth_service.py │ │ ├── company_service.py │ │ └── contact_service.py │ ├─────────────────────────────────────────────────┤ │ Persistence (SQLAlchemy 2.0, app/db/) │ │ ├── models.py (User, Company, Contact) │ │ ├── session.py (engine, SessionLocal) │ │ └── init_db.py (create_all + seed) │ ├─────────────────────────────────────────────────┤ │ SQLite (/data/leocrm.db) │ └─────────────────────────────────────────────────┘ ``` ## 2. Tech-Stack - **Python 3.11** - **FastAPI 0.115** — Web-Framework - **Uvicorn 0.30** — ASGI-Server - **SQLAlchemy 2.0** — ORM - **Jinja2 3.1** — Templates - **bcrypt 4.1** — Passwort-Hashing (über `passlib[bcrypt]`) - **itsdangerous 2.2** — Session-Cookie-Signatur (über Starlette `SessionMiddleware`) - **pytest 8.3** + **httpx 0.27** — Tests - **SQLite 3** (in-Container, Volume `/data`) - **Docker** (Multi-Stage optional; Single-Stage reicht für v0.1) ## 3. Schichten ### 3.1 Routes (app/routes/) - `html_routes.py` — HTML-Endpoints, rendert Jinja2 - `api_routes.py` — JSON-Endpoints, Pydantic-Schemas ### 3.2 Services (app/services/) - `auth_service.py` — `verify_password`, `authenticate_user`, `get_current_user_optional` - `company_service.py` — `list_companies(q, limit, offset)`, `get_company(id)`, `create_company(data)`, `update_company(id, data)`, `delete_company(id)` - `contact_service.py` — analog für Contacts, inkl. Cascade-Delete-Logik ### 3.3 DB (app/db/) - `models.py` — SQLAlchemy-Modelle - `session.py` — `engine = create_engine(...)`, `SessionLocal = sessionmaker(...)`, `get_db()` als FastAPI-Dependency - `init_db.py` — `init_db()` erstellt Tabellen + Seed-Daten, idempotent ### 3.4 Schemas (app/schemas/) - Pydantic-v2-Schemas für Input/Output (CompanyIn, CompanyOut, ContactIn, ContactOut, LoginIn) ## 4. Auth-Flow 1. Browser ruft `/login` → Formular 2. `POST /login` mit `username`+`password` → `auth_service.authenticate_user()` → bcrypt-Vergleich 3. Bei Erfolg: `request.session['user_id'] = user.id` (signed Cookie via Starlette-SessionMiddleware) 4. Redirect auf `/` 5. Alle anderen Routes prüfen `request.session.get('user_id')`; wenn None → 302 → `/login` 6. API macht es analog: `POST /api/auth/login` setzt Cookie, alle anderen API-Routes prüfen Session `SECRET_KEY` wird aus ENV `LEOCRM_SECRET_KEY` gelesen; Fallback-Default nur für lokales Dev, in Coolify **Pflicht-ENV**. ## 5. Datenpersistenz - SQLite-Datei: `/data/leocrm.db` (in Coolify als Volume gemountet) - Schema-Migration: `Base.metadata.create_all(engine)` beim App-Start; Alembic-Migrationen sind v0.2 - Cascade: Contact.company_id → Company.id mit `ondelete='CASCADE'`; beim Company-Delete verschwinden alle Contacts ## 6. Deployment ``` Container: leocrm-app Image: python:3.11-slim + pip install + copy app Port: 8000 Volume: /data (persistent, SQLite) ENV: LEOCRM_SECRET_KEY (required) LEOCRM_DB_PATH (default /data/leocrm.db) Health: GET /api/health ``` Coolify-Setup: 1. Forgejo-Webhook registriert auf `https://forgejo.media-on.de/Leopoldadmin/leocrm` Push auf `main` 2. Coolify-Service vom Typ "Docker Compose" mit `docker-compose.yml` aus dem Repo-Root 3. Domain: `leocrm.media-on.de` mit Let's-Encrypt-SSL ## 7. Sicherheit (v0.1 Minimum) - bcrypt für Passwörter (kein Plain-Text) - Session-Cookie signed, `httponly=True`, `samesite='lax'`, in Prod `secure=True` - CSRF: für v0.1 ausgeschlossen (Login + GET-only-Pattern reicht für Single-User-Demo); v0.2 fügt CSRF-Tokens hinzu - SQL-Injection: durch SQLAlchemy-ORM ausgeschlossen - Input-Validierung: Pydantic-Schemas - Default-User `admin/admin` ist dokumentiert und nur in Demo gedacht ## 8. Skalierbarkeit & Grenzen - v0.1: < 100 Companies, < 1000 Contacts problemlos; darüber SQLite-Performance grenzwertig - v0.2: Migration auf Postgres + Connection-Pool - Stateless-App: horizontal skalierbar; SQLite-Volume wird zum Engpass → Postgres-Migration