diff --git a/DEVLOG.md b/DEVLOG.md index e275158..27cf664 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -843,3 +843,267 @@ Keine Änderungen ermittelbar. - ergebnisse/sprintplan.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --- +## 2026-09-03 22:37 – 22:39 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitea/workflows/ci.yml | 52 ++++++++++++ +- DEVLOG.md | 13 +++ +- backend/.gitignore | 9 +++ +- backend/README.md | 35 +++++++++ +- backend/alembic.ini | 38 +++++++++ +- backend/alembic/env.py | 51 ++++++++++++ +- backend/alembic/script.py.mako | 26 ++++++ +- backend/alembic/versions/0001_initial_schema.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0002_seed_hauptserver.py | 25 ++++++ +- backend/app/__init__.py | 0 +- backend/app/api/__init__.py | 0 +- backend/app/api/deps.py | 52 ++++++++++++ +- backend/app/api/v1/__init__.py | 0 +- backend/app/api/v1/api.py | 7 ++ +- backend/app/api/v1/endpoints/__init__.py | 0 +- backend/app/api/v1/endpoints/auth.py | 55 +++++++++++++ +- backend/app/api/v1/endpoints/health.py | 19 +++++ +- backend/app/core/__init__.py | 0 +- backend/app/core/app_settings.py | 18 +++++ +- backend/app/core/security.py | 33 ++++++++ +- backend/app/db/__init__.py | 0 +- backend/app/db/base.py | 5 ++ +- backend/app/db/session.py | 13 +++ +- backend/app/main.py | 6 ++ +- backend/app/models/__init__.py | 3 + +- backend/app/models/auth.py | 60 ++++++++++++++ +- backend/example.env | 9 +++ +- backend/pyproject.toml | 31 ++++++++ +- backend/pytest.ini | 4 + +- backend/tests/__init__.py | 0 +- backend/tests/conftest.py | 70 +++++++++++++++++ +- backend/tests/test_auth.py | 45 +++++++++++ +- backend/tests/test_health.py | 8 ++ +- backend/tests/test_roles.py | 28 +++++++ + +--- +## 2026-09-03 22:37 – 22:39 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitea/workflows/ci.yml | 52 ++++++++++++ +- DEVLOG.md | 13 +++ +- backend/.gitignore | 9 +++ +- backend/README.md | 35 +++++++++ +- backend/alembic.ini | 38 +++++++++ +- backend/alembic/env.py | 51 ++++++++++++ +- backend/alembic/script.py.mako | 26 ++++++ +- backend/alembic/versions/0001_initial_schema.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0002_seed_hauptserver.py | 25 ++++++ +- backend/app/__init__.py | 0 +- backend/app/api/__init__.py | 0 +- backend/app/api/deps.py | 52 ++++++++++++ +- backend/app/api/v1/__init__.py | 0 +- backend/app/api/v1/api.py | 7 ++ +- backend/app/api/v1/endpoints/__init__.py | 0 +- backend/app/api/v1/endpoints/auth.py | 55 +++++++++++++ +- backend/app/api/v1/endpoints/health.py | 19 +++++ +- backend/app/core/__init__.py | 0 +- backend/app/core/app_settings.py | 18 +++++ +- backend/app/core/security.py | 33 ++++++++ +- backend/app/db/__init__.py | 0 +- backend/app/db/base.py | 5 ++ +- backend/app/db/session.py | 13 +++ +- backend/app/main.py | 6 ++ +- backend/app/models/__init__.py | 3 + +- backend/app/models/auth.py | 60 ++++++++++++++ +- backend/example.env | 9 +++ +- backend/pyproject.toml | 31 ++++++++ +- backend/pytest.ini | 4 + +- backend/tests/__init__.py | 0 +- backend/tests/conftest.py | 70 +++++++++++++++++ +- backend/tests/test_auth.py | 45 +++++++++++ +- backend/tests/test_health.py | 8 ++ +- backend/tests/test_roles.py | 28 +++++++ + +--- +## 2026-09-03 22:37 – 22:39 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitea/workflows/ci.yml | 52 ++++++++++++ +- DEVLOG.md | 13 +++ +- backend/.gitignore | 9 +++ +- backend/README.md | 35 +++++++++ +- backend/alembic.ini | 38 +++++++++ +- backend/alembic/env.py | 51 ++++++++++++ +- backend/alembic/script.py.mako | 26 ++++++ +- backend/alembic/versions/0001_initial_schema.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0002_seed_hauptserver.py | 25 ++++++ +- backend/app/__init__.py | 0 +- backend/app/api/__init__.py | 0 +- backend/app/api/deps.py | 52 ++++++++++++ +- backend/app/api/v1/__init__.py | 0 +- backend/app/api/v1/api.py | 7 ++ +- backend/app/api/v1/endpoints/__init__.py | 0 +- backend/app/api/v1/endpoints/auth.py | 55 +++++++++++++ +- backend/app/api/v1/endpoints/health.py | 19 +++++ +- backend/app/core/__init__.py | 0 +- backend/app/core/app_settings.py | 18 +++++ +- backend/app/core/security.py | 33 ++++++++ +- backend/app/db/__init__.py | 0 +- backend/app/db/base.py | 5 ++ +- backend/app/db/session.py | 13 +++ +- backend/app/main.py | 6 ++ +- backend/app/models/__init__.py | 3 + +- backend/app/models/auth.py | 60 ++++++++++++++ +- backend/example.env | 9 +++ +- backend/pyproject.toml | 31 ++++++++ +- backend/pytest.ini | 4 + +- backend/tests/__init__.py | 0 +- backend/tests/conftest.py | 70 +++++++++++++++++ +- backend/tests/test_auth.py | 45 +++++++++++ +- backend/tests/test_health.py | 8 ++ +- backend/tests/test_roles.py | 28 +++++++ + +--- +## 2026-09-03 22:37 – 22:39 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitea/workflows/ci.yml | 52 ++++++++++++ +- DEVLOG.md | 13 +++ +- backend/.gitignore | 9 +++ +- backend/README.md | 35 +++++++++ +- backend/alembic.ini | 38 +++++++++ +- backend/alembic/env.py | 51 ++++++++++++ +- backend/alembic/script.py.mako | 26 ++++++ +- backend/alembic/versions/0001_initial_schema.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0002_seed_hauptserver.py | 25 ++++++ +- backend/app/__init__.py | 0 +- backend/app/api/__init__.py | 0 +- backend/app/api/deps.py | 52 ++++++++++++ +- backend/app/api/v1/__init__.py | 0 +- backend/app/api/v1/api.py | 7 ++ +- backend/app/api/v1/endpoints/__init__.py | 0 +- backend/app/api/v1/endpoints/auth.py | 55 +++++++++++++ +- backend/app/api/v1/endpoints/health.py | 19 +++++ +- backend/app/core/__init__.py | 0 +- backend/app/core/app_settings.py | 18 +++++ +- backend/app/core/security.py | 33 ++++++++ +- backend/app/db/__init__.py | 0 +- backend/app/db/base.py | 5 ++ +- backend/app/db/session.py | 13 +++ +- backend/app/main.py | 6 ++ +- backend/app/models/__init__.py | 3 + +- backend/app/models/auth.py | 60 ++++++++++++++ +- backend/example.env | 9 +++ +- backend/pyproject.toml | 31 ++++++++ +- backend/pytest.ini | 4 + +- backend/tests/__init__.py | 0 +- backend/tests/conftest.py | 70 +++++++++++++++++ +- backend/tests/test_auth.py | 45 +++++++++++ +- backend/tests/test_health.py | 8 ++ +- backend/tests/test_roles.py | 28 +++++++ + +--- +## 2026-09-03 22:53 – 22:54 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitea/workflows/ci.yml | 52 ++++++++++++ +- DEVLOG.md | 13 +++ +- backend/.gitignore | 9 +++ +- backend/README.md | 35 +++++++++ +- backend/alembic.ini | 38 +++++++++ +- backend/alembic/env.py | 51 ++++++++++++ +- backend/alembic/script.py.mako | 26 ++++++ +- backend/alembic/versions/0001_initial_schema.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0002_seed_hauptserver.py | 25 ++++++ +- backend/app/__init__.py | 0 +- backend/app/api/__init__.py | 0 +- backend/app/api/deps.py | 52 ++++++++++++ +- backend/app/api/v1/__init__.py | 0 +- backend/app/api/v1/api.py | 7 ++ +- backend/app/api/v1/endpoints/__init__.py | 0 +- backend/app/api/v1/endpoints/auth.py | 55 +++++++++++++ +- backend/app/api/v1/endpoints/health.py | 19 +++++ +- backend/app/core/__init__.py | 0 +- backend/app/core/app_settings.py | 18 +++++ +- backend/app/core/security.py | 33 ++++++++ +- backend/app/db/__init__.py | 0 +- backend/app/db/base.py | 5 ++ +- backend/app/db/session.py | 13 +++ +- backend/app/main.py | 6 ++ +- backend/app/models/__init__.py | 3 + +- backend/app/models/auth.py | 60 ++++++++++++++ +- backend/example.env | 9 +++ +- backend/pyproject.toml | 31 ++++++++ +- backend/pytest.ini | 4 + +- backend/tests/__init__.py | 0 +- backend/tests/conftest.py | 70 +++++++++++++++++ +- backend/tests/test_auth.py | 45 +++++++++++ +- backend/tests/test_health.py | 8 ++ +- backend/tests/test_roles.py | 28 +++++++ + +--- +## 2026-09-03 22:54 – 22:55 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .gitea/workflows/ci.yml | 52 ++++++++++++ +- DEVLOG.md | 13 +++ +- backend/.gitignore | 9 +++ +- backend/README.md | 35 +++++++++ +- backend/alembic.ini | 38 +++++++++ +- backend/alembic/env.py | 51 ++++++++++++ +- backend/alembic/script.py.mako | 26 ++++++ +- backend/alembic/versions/0001_initial_schema.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- backend/alembic/versions/0002_seed_hauptserver.py | 25 ++++++ +- backend/app/__init__.py | 0 +- backend/app/api/__init__.py | 0 +- backend/app/api/deps.py | 52 ++++++++++++ +- backend/app/api/v1/__init__.py | 0 +- backend/app/api/v1/api.py | 7 ++ +- backend/app/api/v1/endpoints/__init__.py | 0 +- backend/app/api/v1/endpoints/auth.py | 55 +++++++++++++ +- backend/app/api/v1/endpoints/health.py | 19 +++++ +- backend/app/core/__init__.py | 0 +- backend/app/core/app_settings.py | 18 +++++ +- backend/app/core/security.py | 33 ++++++++ +- backend/app/db/__init__.py | 0 +- backend/app/db/base.py | 5 ++ +- backend/app/db/session.py | 13 +++ +- backend/app/main.py | 6 ++ +- backend/app/models/__init__.py | 3 + +- backend/app/models/auth.py | 60 ++++++++++++++ +- backend/example.env | 9 +++ +- backend/pyproject.toml | 31 ++++++++ +- backend/pytest.ini | 4 + +- backend/tests/__init__.py | 0 +- backend/tests/conftest.py | 70 +++++++++++++++++ +- backend/tests/test_auth.py | 45 +++++++++++ +- backend/tests/test_health.py | 8 ++ +- backend/tests/test_roles.py | 28 +++++++ + +--- diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 1aeae2e..f8e7373 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -41,7 +41,7 @@ async def login( detail="Login oder Passwort falsch", headers={"WWW-Authenticate": "Bearer"}, ) - token = create_access_token(subject=benutzer.login, roles=benutzer.rollen_namen) + token = create_access_token(subject=benutzer.login) return TokenResponse(access_token=token) diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 57e63c5..eb525b7 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -16,9 +16,11 @@ def verify_password(plain_password: str, password_hash: str) -> bool: return _pwd_context.verify(plain_password, password_hash) -def create_access_token(*, subject: str, roles: list[str]) -> str: +def create_access_token(*, subject: str) -> str: + # Bewusst KEINE Rollen im Token: get_current_user liest Rollen bei jedem Request + # frisch aus der DB (Rollenänderung wirkt sofort, kein Token-Refresh nötig). expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) - payload = {"sub": subject, "roles": roles, "exp": expire} + payload = {"sub": subject, "exp": expire} return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) diff --git a/backend/app/db/session.py b/backend/app/db/session.py index 8ed7f9e..b233c7d 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -9,5 +9,16 @@ SessionLocal = async_sessionmaker(engine, expire_on_commit=False) async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Konvention für alle Endpunkte: Erfolg -> commit, Exception -> rollback. + + Endpunkte selbst rufen kein `db.commit()`/`db.rollback()` auf, sondern verlassen + sich auf diese Dependency (Review-Fund: Konvention muss zentral, nicht pro + Endpunkt copy-paste sein). + """ async with SessionLocal() as session: - yield session + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise diff --git a/backend/app/main.py b/backend/app/main.py index 5cb5c2e..67728ee 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,24 @@ +from contextlib import asynccontextmanager + from fastapi import FastAPI from app.api.v1.api import api_router +from app.core.app_settings import settings +from app.db.session import engine -app = FastAPI(title="MABEA", version="0.1.0") +_PLACEHOLDER_JWT_SECRET = "change-me-to-a-long-random-value" + + +@asynccontextmanager +async def lifespan(app: FastAPI): + if not settings.jwt_secret_key or settings.jwt_secret_key == _PLACEHOLDER_JWT_SECRET: + raise RuntimeError( + "JWT_SECRET_KEY ist leer oder noch der Platzhalter aus example.env – " + "in .env auf dem Zielsystem einen echten zufälligen Wert setzen." + ) + yield + await engine.dispose() + + +app = FastAPI(title="MABEA", version="0.1.0", lifespan=lifespan) app.include_router(api_router, prefix="/api/v1") diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 3677e1a..1d9f53e 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -18,14 +18,26 @@ TestSessionLocal = async_sessionmaker(engine, expire_on_commit=False) @pytest_asyncio.fixture async def db_session(): + """Jeder Test läuft in einer äußeren Transaktion, die am Ende zurückgerollt wird. + + Die Session ist per `join_transaction_mode="create_savepoint"` an die äußere + Connection gebunden: ein `commit()` innerhalb des Tests (z. B. durch `get_db`, + das jetzt selbst committet) schließt nur eine SAVEPOINT ab, nicht die äußere + Transaktion – das äußere `connection.rollback()` verwirft am Ende trotzdem alles. + (SQLAlchemy 2.0 "Joining a Session into an External Transaction" – Review-Fund + fastapi-expert: ohne dieses Pattern hätte ein commit() in Endpunkten die + Testisolation gebrochen.) + """ async with engine.connect() as connection: - transaction = await connection.begin() - session = AsyncSession(bind=connection, expire_on_commit=False) + await connection.begin() + session = AsyncSession( + bind=connection, expire_on_commit=False, join_transaction_mode="create_savepoint" + ) try: yield session finally: await session.close() - await transaction.rollback() + await connection.rollback() @pytest_asyncio.fixture diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..01db694 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,27 @@ +# Deployment – MABEA + +Zielserver: siehe Projekt-Memory (Debian 13, kein Docker-Zwang, Prompt 19). + +## Server-Grundausstattung (einmalig) + +```bash +scp -i ~/.ssh/id_claude -r deploy root@:/root/mabea-deploy +ssh -i ~/.ssh/id_claude root@ +cd /root/mabea-deploy +chmod +x install_server.sh +DB_PASSWORD='' APP_DOMAIN= ./install_server.sh +``` + +Installiert PostgreSQL + nginx, legt DB-Rolle/Datenbank an, richtet nginx als Reverse-Proxy vor `127.0.0.1:8000` ein. Ausgegebene `DATABASE_URL` sichern. + +## Backend deployen (später, nach Sprint-Fortschritt) + +1. `backend/` auf den Server kopieren (z. B. nach `/opt/mabea/backend`), `.venv` dort anlegen, `pip install -e ".[dev]"` bzw. ohne `dev`-Extra für Produktion. +2. `.env` aus `backend/example.env` ableiten, `DATABASE_URL` aus Schritt oben eintragen. +3. `alembic upgrade head` ausführen. +4. `mabea-backend.service.template` nach `/etc/systemd/system/mabea-backend.service` kopieren (Pfade/User anpassen), `systemctl daemon-reload && systemctl enable --now mabea-backend`. + +## Dateien +- `install_server.sh` – Server-Grundausstattung (PostgreSQL, nginx), idempotent +- `nginx_mabea.conf.template` – Reverse-Proxy-Vorlage, von `install_server.sh` verwendet +- `mabea-backend.service.template` – systemd-Unit-Vorlage für das Backend (noch nicht aktiviert, Pfade anpassen) diff --git a/deploy/install_server.sh b/deploy/install_server.sh new file mode 100755 index 0000000..4092e99 --- /dev/null +++ b/deploy/install_server.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# MABEA – Server-Setup (Debian 13). Installiert PostgreSQL + nginx, legt DB/Rolle an, +# richtet nginx als Reverse-Proxy vor dem späteren FastAPI/Uvicorn-Backend ein. +# Idempotent: mehrfacher Aufruf überschreibt keine bestehende DB-Rolle/DB. +# +# Ausführung auf dem Zielserver als root: +# DB_PASSWORD=... APP_DOMAIN=mabea.example.org ./install_server.sh +# Ohne DB_PASSWORD wird ein zufälliges Passwort erzeugt und am Ende ausgegeben +# (sofort sichern, z. B. in backend/.env auf dem Server – nicht committen). + +set -euo pipefail + +DB_NAME="${DB_NAME:-mabea}" +DB_USER="${DB_USER:-mabea}" +DB_PASSWORD="${DB_PASSWORD:-$(openssl rand -base64 24)}" +JWT_SECRET_KEY="${JWT_SECRET_KEY:-$(openssl rand -base64 48)}" +APP_DOMAIN="${APP_DOMAIN:-_}" # "_" = nginx catch-all, solange keine Domain feststeht + +echo "== apt update/install ==" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq postgresql nginx python3-venv python3-pip git openssl + +echo "== PostgreSQL: Rolle + Datenbank ==" +if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then + sudo -u postgres psql -c "CREATE ROLE ${DB_USER} WITH LOGIN PASSWORD '${DB_PASSWORD}';" + echo "Rolle ${DB_USER} angelegt." +else + echo "Rolle ${DB_USER} existiert bereits, überspringe." +fi + +if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then + sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" + echo "Datenbank ${DB_NAME} angelegt." +else + echo "Datenbank ${DB_NAME} existiert bereits, überspringe." +fi + +# gen_random_uuid() ist seit PG13 built-in, aber pgcrypto schadet nicht und macht es +# explizit/robust gegen ältere Backports (Review-Fund postgres-expert). +sudo -u postgres psql -d "${DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" + +echo "== PostgreSQL: Speicher-Tuning für kleinen VPS (4GB RAM) ==" +PG_CONF_DIR=$(sudo -u postgres psql -tAc "SHOW config_file;" | xargs dirname) +mkdir -p "${PG_CONF_DIR}/conf.d" +cat > "${PG_CONF_DIR}/conf.d/mabea-tuning.conf" <<'PGCONF' +# MABEA: konservatives Tuning für ~4GB RAM VPS, nicht enterprise-scale. +shared_buffers = 1GB +effective_cache_size = 3GB +maintenance_work_mem = 256MB +work_mem = 8MB +max_connections = 50 +PGCONF +grep -q "conf.d/\*.conf" "${PG_CONF_DIR}/postgresql.conf" || \ + echo "include_dir = 'conf.d'" >> "${PG_CONF_DIR}/postgresql.conf" + +echo "== nginx Reverse-Proxy Config (inkl. Security-Header) ==" +sed -e "s/__APP_DOMAIN__/${APP_DOMAIN}/g" \ + "$(dirname "$0")/nginx_mabea.conf.template" > /etc/nginx/sites-available/mabea.conf +ln -sf /etc/nginx/sites-available/mabea.conf /etc/nginx/sites-enabled/mabea.conf +rm -f /etc/nginx/sites-enabled/default +nginx -t + +echo "== Dienste aktivieren ==" +systemctl enable --now postgresql +systemctl restart postgresql # Tuning-Konfiguration laden +systemctl enable --now nginx +systemctl reload nginx + +echo "== Zugangsdaten sichern ==" +CRED_FILE="/root/mabea_db_credentials.env" +umask 077 +cat > "${CRED_FILE}" <