banyaro/Makefile
rene 9394bab1fb Big Sweep: Security + Race-Conditions + Tests + DSGVO + A11y, SW by-v1095
SECURITY (auth.py, routes/auth.py, database.py, main.py)
- JWT bekommt jti; Logout trägt in neue jwt_blacklist-Tabelle ein,
  decode_token() prüft → server-side Invalidierung
- JWT-Expiry default 30 → 7 Tage (ENV JWT_EXPIRY_DAYS überschreibt)
- Sliding-Refresh-Middleware: erneuert Cookie wenn >50% verbraucht
  (Schwelle via JWT_REFRESH_FRACTION, Default 2)
- Login-Lockout in DB-Tabelle login_attempts (5 Versuche / 15 Min,
  überlebt Container-Restart) — alte In-Memory-Lockouts ersetzt
- SMTP-Versand: alle 'except: pass' durch logger.exception ersetzt;
  Fehlversuche landen in failed_emails-Tabelle für späteres Retry
- Referral-Counter Race gefixt: UPDATE partner_codes SET uses=uses+1
  ... WHERE uses<max_uses RETURNING — atomar statt SELECT+UPDATE

RACE CONDITIONS (routes/invoices.py, database.py)
- Neue invoice_counters-Tabelle für atomare Nummernvergabe
- _next_invoice_number nutzt BEGIN IMMEDIATE + atomares UPDATE
- Funktioniert für RG- und ST-Prefixe (Stornorechnungen)
- Race-Test verifiziert (5 Threads × 20 Calls = 100 eindeutige Nummern)

VERSION + TESTS + ERROR-DIGEST (VERSION, Makefile, tests/, scheduler.py)
- Neue VERSION-Datei (Single Source of Truth) — main.py liest beim
  Startup
- Makefile-Target 'make bump' propagiert in sw.js, app.js, index.html
- Makefile-Target 'make test' setzt venv auf, läuft pytest
- 19 Smoke-Tests in tests/ (health, auth, diary, invoice) — alle grün
- Scheduler: täglicher _job_error_digest um 06:30 → schickt Error-
  Zusammenfassung an ADMIN_EMAIL (still wenn keine Errors)

DSGVO + A11Y + ERSTE-HILFE
- landing.html: 'HTML und ODS' → 'JSON' (tatsächlich implementiert)
- datenschutz.js: Sektion Account-Löschung erweitert (sofort gelöscht /
  anonymisiert / 10 Jahre für Rechnungen)
- erste-hilfe.js: prominentes Warning-Banner oben (ersetzt keine
  Tierarzt-Beratung); Notfallnummern gruppiert nach Land, TODO-Platz-
  halter für AT-Uni-Klinik, CH Tox Info Suisse, CH Tierspital Zürich
- ui.js Modal: ESC schließt, Focus-Trap, Auto-Focus erstes Element,
  Restore Focus auf vorigen Caller
- impressum.js Kontaktformular: Labels mit for=cf-name etc.

NEUE DB-TABELLEN (idempotent via CREATE TABLE IF NOT EXISTS)
- jwt_blacklist, login_attempts, failed_emails, invoice_counters

NEUE ENV-VARS
- JWT_REFRESH_FRACTION (Default 2)
- JWT_EXPIRY_DAYS Default geändert (30 → 7)
2026-05-26 20:12:01 +02:00

310 lines
12 KiB
Makefile

# ==============================================================
# BAN YARO — Makefile
# Deploy-Strategie: rsync vom Mac zur DS (kein git auf DS nötig)
# Git wird nur lokal auf dem Mac und für Forgejo-Backup genutzt.
# ==============================================================
DS_HOST := ds
DS_IP := 10.47.11.10
# Hinweis: NPM braucht 10.47.11.99 als Forward-IP (Macvlan-Shim), nicht .10
DS_SSH_PORT := 4711
DS_PATH := /volume1/docker/banyaro
DS_PATH_STAGING := /volume1/docker/banyaro-staging
CONTAINER := banyaro
CONTAINER_STAGING:= banyaro-staging
SERVICE := banyaro
GIT_REMOTE := origin
DOCKER := sudo /usr/local/bin/docker
VERSION ?= $(shell grep '"version"' backend/static/manifest.json | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | head -1)
# tar: was NICHT auf die DS übertragen wird
TAR_EXCLUDE := --exclude='.git' \
--exclude='./data' \
--exclude='./backend/venv' \
--exclude='./backend/__pycache__' \
--exclude='./.env' \
--exclude='./*.db' \
--exclude='./.DS_Store'
.PHONY: help deploy deploy-clean staging release sync push restart build stop status \
logs logs-f shell db dev clean-cache check-ssh reports bump test
# ----------------------------------------------------------
# SSH-Prüfung — Abhängigkeit aller DS-Befehle
# ----------------------------------------------------------
check-ssh:
@if ! nc -z -w3 $(DS_IP) $(DS_SSH_PORT) 2>/dev/null; then \
echo ""; \
echo " ✗ DS nicht erreichbar ($(DS_IP):$(DS_SSH_PORT))"; \
echo ""; \
echo " SSH-Port $(DS_SSH_PORT) ist geschlossen oder DS offline."; \
echo " → Port in Fritz!Box / DS-Firewall öffnen, dann erneut versuchen."; \
echo ""; \
exit 1; \
fi
# ----------------------------------------------------------
# Hilfe
# ----------------------------------------------------------
help:
@echo ""
@echo " Ban Yaro — verfügbare Befehle:"
@echo ""
@echo " make deploy Sync + Rebuild + Neustart (mit Layer-Cache, häufigster Befehl)"
@echo " make deploy-clean Wie deploy, aber --no-cache (bei requirements-Änderungen)"
@echo " make sync Nur Dateien zur DS übertragen (ohne Rebuild)"
@echo " make push Nur Git push zu Forgejo (Backup/Versionierung)"
@echo " make restart Container neustarten (kein Rebuild)"
@echo " make build Docker neu bauen (ohne Neustart)"
@echo " make stop Container stoppen"
@echo " make status Container-Status anzeigen"
@echo ""
@echo " make logs Letzte 100 Zeilen"
@echo " make logs-f Live-Log-Stream"
@echo " make shell Bash im Container"
@echo " make db SQLite-Shell"
@echo ""
@echo " make dev Lokaler Dev-Server auf Mac (Port 8001)"
@echo " make clean-cache SW-Cache-Version erhöhen + restart"
@echo " make reports Quartalsberichte generieren + committen"
@echo ""
# ----------------------------------------------------------
# DEPLOY — der Haupt-Befehl
# 1. Git push (Backup)
# 2. rsync zum DS
# 3. Docker rebuild + restart
# ----------------------------------------------------------
deploy: check-ssh
@echo "→ Git push (Backup)..."
@git push $(GIT_REMOTE) main
@echo "→ Sync zu DS..."
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/"
@echo "→ Docker rebuild + restart (mit Layer-Cache)..."
@ssh $(DS_HOST) " \
cd $(DS_PATH) && \
$(DOCKER) compose down && \
$(DOCKER) compose build && \
$(DOCKER) compose up -d"
@echo ""
@echo " ✓ Deploy fertig. Letzte Logs:"
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER) --tail=15"
# ----------------------------------------------------------
# DEPLOY-CLEAN — erzwingt Neuaufbau ohne Layer-Cache
# Nötig wenn: requirements.txt geändert, System-Pakete aktualisiert,
# oder nach einem kaputten Image-State
# ----------------------------------------------------------
deploy-clean: check-ssh
@echo "→ Git push (Backup)..."
@git push $(GIT_REMOTE) main
@echo "→ Sync zu DS..."
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/"
@echo "→ Docker rebuild (--no-cache) + restart..."
@ssh $(DS_HOST) " \
cd $(DS_PATH) && \
$(DOCKER) compose down && \
$(DOCKER) compose build --no-cache && \
$(DOCKER) compose up -d"
@echo ""
@echo " ✓ Deploy fertig. Letzte Logs:"
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER) --tail=15"
# ----------------------------------------------------------
# STAGING — develop-Branch auf Staging deployen (Port 3012)
# ----------------------------------------------------------
staging: check-ssh
@echo "→ Git push develop..."
@git push $(GIT_REMOTE) develop
@echo "→ Sync zu DS (Staging)..."
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH_STAGING)/"
@echo "→ Staging rebuild + restart..."
@ssh $(DS_HOST) " \
cd $(DS_PATH_STAGING) && \
$(DOCKER) compose -f docker-compose.staging.yml down && \
$(DOCKER) compose -f docker-compose.staging.yml build && \
$(DOCKER) compose -f docker-compose.staging.yml up -d"
@echo ""
@echo " ✓ Staging fertig — https://staging.banyaro.app"
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_STAGING) --tail=10"
# ----------------------------------------------------------
# STAGING-DB — Produktions-DB in Staging kopieren (interaktiv, braucht sudo)
# Aufruf: make staging-db
# ----------------------------------------------------------
staging-db: check-ssh
@echo "→ Produktions-DB nach Staging kopieren..."
@ssh -t $(DS_HOST) " \
sudo cp $(DS_PATH)/data/banyaro.db $(DS_PATH_STAGING)/data/banyaro.db && \
sudo chmod 666 $(DS_PATH_STAGING)/data/banyaro.db && \
echo '✓ DB kopiert'"
# ----------------------------------------------------------
# RELEASE — develop → main → Production (VERSION= pflichtangabe)
# Beispiel: make release VERSION=1.1.0
# ----------------------------------------------------------
release: check-ssh
@if [ -z "$(VERSION)" ]; then \
echo "❌ Bitte VERSION setzen: make release VERSION=1.1.0"; exit 1; fi
@echo "→ Merge develop → main (v$(VERSION))..."
@git checkout main
@git merge develop --no-ff -m "Release v$(VERSION)"
@sed -i '' 's/"version": "[^"]*"/"version": "$(VERSION)"/' backend/static/manifest.json
@sed -i '' "s/const APP_VERSION = '[^']*'/const APP_VERSION = '$(VERSION)'/" backend/static/js/app.js
@git add backend/static/manifest.json backend/static/js/app.js
@git commit --amend --no-edit
@git tag "v$(VERSION)"
@git push $(GIT_REMOTE) main --tags
@echo "→ Production deployen..."
@$(MAKE) deploy
@git checkout develop
@git merge main
@git push $(GIT_REMOTE) develop
@echo ""
@echo " ✓ Release v$(VERSION) veröffentlicht 🚀"
# ----------------------------------------------------------
# SYNC — nur Dateien zur DS übertragen, kein Docker-Rebuild
# ACHTUNG: ALLE Dateien (CSS/JS/HTML/Python) sind ins Image gebacken!
# sync+restart reicht für NICHTS — immer: make deploy
# ----------------------------------------------------------
sync: check-ssh
@echo "→ Sync zu DS..."
@COPYFILE_DISABLE=1 tar czf - $(TAR_EXCLUDE) . | ssh $(DS_HOST) "tar xzf - -C $(DS_PATH)/"
@echo " ✓ Sync fertig."
# ----------------------------------------------------------
# PUSH — nur Git, ohne DS-Deploy
# ----------------------------------------------------------
push:
git push $(GIT_REMOTE) main
# ----------------------------------------------------------
# RESTART — kein Rebuild, nur Container neu starten
# Reicht nur für Umgebungsvariablen-Änderungen (.env)
# ----------------------------------------------------------
restart: check-ssh
@ssh $(DS_HOST) " \
cd $(DS_PATH) && \
$(DOCKER) compose restart $(SERVICE)"
@echo " ✓ Neugestartet."
# ----------------------------------------------------------
# BUILD — nur Docker-Image neu bauen (mit Layer-Cache)
# ----------------------------------------------------------
build: check-ssh
@ssh $(DS_HOST) " \
cd $(DS_PATH) && \
$(DOCKER) compose build"
# ----------------------------------------------------------
# STOP
# ----------------------------------------------------------
stop: check-ssh
@ssh $(DS_HOST) "cd $(DS_PATH) && $(DOCKER) compose down"
@echo " ✓ Gestoppt."
# ----------------------------------------------------------
# STATUS
# ----------------------------------------------------------
status: check-ssh
@ssh $(DS_HOST) "$(DOCKER) ps \
--filter name=$(CONTAINER) \
--format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
# ----------------------------------------------------------
# LOGS
# ----------------------------------------------------------
logs: check-ssh
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER) --tail=100"
logs-f: check-ssh
@ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER) -f"
# ----------------------------------------------------------
# SHELL im Container
# ----------------------------------------------------------
shell: check-ssh
@ssh -t $(DS_HOST) "$(DOCKER) exec -it $(CONTAINER) bash"
# ----------------------------------------------------------
# SQLite
# ----------------------------------------------------------
db: check-ssh
@ssh -t $(DS_HOST) "$(DOCKER) exec -it $(CONTAINER) \
sqlite3 /data/banyaro.db"
# ----------------------------------------------------------
# DEV — lokaler Server auf Mac (kein Docker, kein DS)
# ----------------------------------------------------------
dev:
@cd backend && \
test -d venv || python3 -m venv venv && \
. venv/bin/activate && \
pip install -q -r requirements.txt && \
KI_MODE=off ENV=development \
JWT_SECRET=dev-secret \
DB_PATH=./dev.db \
uvicorn main:app --reload --port 8001
# ----------------------------------------------------------
# REPORTS — Quartalsberichte generieren und committen
# Berichte laufen im Container (DB-Zugriff), werden lokal gespeichert
# ----------------------------------------------------------
REPORT_DATE := $(shell date +%Y-%m-%d)
REPORT_SECTIONS := sicherheit funktionsumfang dateien nutzer partner server
reports: check-ssh
@mkdir -p reports
@echo "→ Berichte generieren ($(REPORT_DATE))..."
@for section in $(REPORT_SECTIONS); do \
echo "$$section..."; \
ssh $(DS_HOST) "$(DOCKER) exec $(CONTAINER) python3 scripts/generate_reports.py $$section" \
> reports/$(REPORT_DATE)-$$section.md; \
done
@echo "→ Berichte committen..."
@git add reports/
@git diff --cached --quiet || git commit -m "Reports $(REPORT_DATE) — Quartalsbericht"
@echo ""
@echo " ✓ Alle Berichte erstellt und committed:"
@for section in $(REPORT_SECTIONS); do \
echo " reports/$(REPORT_DATE)-$$section.md"; \
done
# ----------------------------------------------------------
# BUMP — zentrale Versions-Erhöhung (VERSION-Datei + sw.js + app.js + index.html)
# Aufruf:
# make bump → liest aus VERSION, erhöht +1, schreibt zurück, propagiert in alle Frontend-Stellen
# make bump APP_VER=2000 → setzt VERSION explizit auf 2000
# Backend liest APP_VER beim Startup aus VERSION (siehe main.py:_read_app_ver()).
# ----------------------------------------------------------
bump:
@if [ ! -f VERSION ]; then echo "0" > VERSION; fi
@CUR=$$(cat VERSION | tr -d '[:space:]'); \
if [ -n "$(APP_VER)" ]; then NEW="$(APP_VER)"; else NEW=$$(($$CUR + 1)); fi; \
printf "%s" "$$NEW" > VERSION; \
sed -i.bak -E "s/const VER[[:space:]]*=[[:space:]]*'[0-9]+'/const VER = '$$NEW'/" backend/static/sw.js && rm -f backend/static/sw.js.bak; \
sed -i.bak -E "s/const APP_VER[[:space:]]*=[[:space:]]*'[0-9]+'/const APP_VER = '$$NEW'/" backend/static/js/app.js && rm -f backend/static/js/app.js.bak; \
sed -i.bak -E "s/\?v=[0-9]+/?v=$$NEW/g" backend/static/index.html && rm -f backend/static/index.html.bak; \
echo " ✓ APP_VER $$CUR → $$NEW (VERSION, sw.js, app.js, index.html aktualisiert)"
# ----------------------------------------------------------
# TEST — Smoke-Tests gegen isolierte Test-DB (kein Docker, kein DS)
# ----------------------------------------------------------
test:
@cd backend && test -d venv || python3 -m venv venv
@backend/venv/bin/pip install -q -r backend/requirements.txt pytest pytest-asyncio
@backend/venv/bin/python -m pytest -q
# ----------------------------------------------------------
# CACHE leeren — SW-Version erhöhen, dann restart
# Nach größeren CSS/JS-Änderungen wenn SW gecacht hat
# ----------------------------------------------------------
clean-cache: check-ssh
@NEW_VER="by-v$$(date +%s)"; \
ssh $(DS_HOST) " \
sed -i \"s/by-v[0-9]*/$$NEW_VER/g\" \
$(DS_PATH)/backend/static/sw.js"
@$(MAKE) restart
@echo " ✓ Cache-Version erhöht."