Files
ApiCore/ApiCore-Backend/.env.example
T
Nick SzaunigandClaude Opus 5.5 1971b2d9c4 feat: T4b/T4c wave 8 - weather history, phenology, honey chain, Telegram base
- Weather history from Open-Meteo behind an adapter: worker loop
  phenology-weather with backoff, Retry-After, User-Agent, chunked
  writes and forecast-to-archive correction
- Phenology: growing degree sum per DWD, phase from reports, GTS and
  calendar, calibration from previous years, daily job step,
  SeasonService reads the stored state
- Honey chain: prefill for extractions and batches (proposals only),
  plausibility and withdrawal warnings, Extraction lock, chain events
  and batch.created webhook from HoneyBatchRecorded
- Telegram base: schema, NotificationDelivery outbox, hardened bot
  client, startup check without crash, no-network-outside-loops guard
- ARCHITECTURE.md: sections 11.11-11.13, assumptions 42-46, env table

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 20:00:46 +02:00

136 lines
7.0 KiB
Bash

# Kopieren nach .env und mit echten Werten befuellen. .env ist gitignored.
DATABASE_URL="postgresql://user:password@localhost:5432/apicore?schema=public"
# Mindestens 32 Zeichen (server-seitig erzwungen, siehe
# src/runtime/startup-checks.ts) - signiert jedes Access-/MFA-/SSO-Token.
# TOTP- und Webhook-Secrets aus der Zeit vor den eigenen Schluesseln (unten)
# sind noch damit verschluesselt: erst nach dem rekey-Job rotieren.
# Erzeugen z.B. mit: openssl rand -hex 32
JWT_ACCESS_SECRET="change-me-to-a-random-value-at-least-32-characters-long"
JWT_REFRESH_SECRET="change-me-refresh-secret"
JWT_ACCESS_EXPIRES_IN="15m"
JWT_REFRESH_EXPIRES_IN="7d"
# Hard limit from login, however often the session is refreshed.
SESSION_MAX_AGE="7d"
# New passwords are checked against known breaches (Have I Been Pwned,
# k-anonymity: only 5 hash characters leave the server). "false" turns the
# check off for deployments without internet access.
# PASSWORD_BREACH_CHECK=true
PORT=3001
FRONTEND_ORIGIN="http://localhost:3000"
# Reverse-proxy hops in front of the backend whose X-Forwarded-For is
# trusted (client IP for rate limits and API-key IP allowlists). "1" for the
# single K8s ingress; a list such as "loopback, 10.0.0.0/8" also works.
# Behind Cloudflare + Traefik use "loopback, <pod CIDR>, <Cloudflare ranges>"
# (see deploy/k8s/base/backend/configmap.example.yaml); "1" would yield the
# Cloudflare edge IP that many clients share.
TRUST_PROXY=1
# Refresh-token cookie requires HTTPS by default. Set to "false" only for
# local HTTP development - every real deployment must leave this unset (or
# "true").
COOKIE_SECURE=false
# Lokales Verzeichnis fuer hochgeladene Dokumente/Bilder (Modul 29).
# Bei horizontaler Skalierung (>1 Replica) muss dies auf ein geteiltes
# Volume (z.B. ReadWriteMany-PVC) zeigen, siehe ARCHITECTURE.md Annahme 7.
DOCUMENTS_STORAGE_PATH="./uploads"
# Automatische Backups (Modul Backup). Alle drei Werte sind auch ueber die
# GUI (Verwaltung > Backup) konfigurierbar - eine hier gesetzte Env-Variable
# hat aber IMMER Vorrang vor dem GUI-Wert, pro Feld einzeln. Ohne Pfad
# (weder Env noch GUI) bleibt der Zeitplan deaktiviert (nur manueller
# Export/Import ueber die UI verfuegbar).
# Bei horizontaler Skalierung (>1 Replica) muss der Pfad auf ein geteiltes
# Volume zeigen (wie DOCUMENTS_STORAGE_PATH), sonst sieht jede Replica nur
# ihre eigenen Backup-Dateien und die Lauf-Erkennung zwischen Replicas greift
# nicht.
BACKUP_STORAGE_PATH="./backups"
# Directory a backup path set in the admin UI must stay under (the env
# variable above is not restricted). Default "./backups".
# BACKUP_ROOT="./backups"
# Cron-Ausdruck (Standard: taeglich um 03:00 Uhr).
BACKUP_CRON_SCHEDULE="0 3 * * *"
# Wie viele der neuesten Backup-Dateien behalten werden, aeltere werden nach
# jedem Lauf geloescht (Standard: 14).
BACKUP_RETENTION_COUNT=14
# Encryption keys, one per purpose (AES-256-GCM, key derived per purpose with
# HKDF): TOTP secrets, webhook signing secrets, backup files. Required in
# production: at least 32 characters each, all three different from each
# other and from JWT_ACCESS_SECRET/JWT_REFRESH_SECRET, e.g.
# "openssl rand -hex 32". Outside production a missing key falls back to one
# derived from JWT_ACCESS_SECRET.
# Rotation: move the old value to <NAME>_PREVIOUS (still opens), set a new
# <NAME>, run the rekey job, then drop <NAME>_PREVIOUS. Backup files are not
# resealed: keep BACKUP_ENCRYPTION_KEY_PREVIOUS while older files must restore.
# Set the same BACKUP_ENCRYPTION_KEY on another deployment to restore its
# backups there (MFA_/WEBHOOK_ENCRYPTION_KEY too, to keep MFA and webhook
# secrets). Keep the backup key outside the backup storage.
# MFA_ENCRYPTION_KEY=""
# WEBHOOK_ENCRYPTION_KEY=""
# BACKUP_ENCRYPTION_KEY=""
# MFA_ENCRYPTION_KEY_PREVIOUS=""
# WEBHOOK_ENCRYPTION_KEY_PREVIOUS=""
# BACKUP_ENCRYPTION_KEY_PREVIOUS=""
# Entra ID SSO. Ohne alle vier Werte bleibt SSO deaktiviert
# (GET /api/v1/auth/sso/status meldet enabled: false, kein Fehler beim Start).
# OIDC_ISSUER_URL ist die v2.0-Metadaten-URL des Tenants, OIDC_REDIRECT_URI
# muss exakt der in der App-Registrierung hinterlegten Redirect-URI entsprechen.
#
# Dieselbe App-Registrierung wird auch fuer die M365-Kalender-Synchronisation
# (Modul Kalender) wiederverwendet - kein zweites Enterprise-App-Objekt noetig.
# Dafuer in der App-Registrierung zusaetzlich die Application-Permission
# "Calendars.ReadWrite" hinzufuegen (Admin-Consent noetig); die Tenant-ID wird
# aus OIDC_ISSUER_URL abgeleitet. Eintraege (Aufgaben, Durchsichten,
# Behandlungen, Fuetterungen, Ernten, Koeniginnen-Zuordnungen) werden
# einseitig (ApiCore -> M365) in den Kalender einer Microsoft-365-Gruppe
# uebertragen. Ohne diese drei Werte bleibt die Synchronisation deaktiviert;
# zusaetzlich muessen die Synchronisation selbst UND die jeweilige Gruppen-ID
# in der GUI (Verwaltung > Automatisierung > Kalender-Sync) gesetzt werden.
OIDC_ISSUER_URL="https://login.microsoftonline.com/<tenant-id>/v2.0"
OIDC_CLIENT_ID="change-me-entra-client-id"
OIDC_CLIENT_SECRET="change-me-entra-client-secret"
OIDC_REDIRECT_URI="http://localhost:3001/api/v1/auth/sso/callback"
# Swagger-UI unter /api/v1/docs ist per Default deaktiviert, da sie das
# gesamte API-Schema ungeschuetzt (kein JwtAuthGuard) preisgeben wuerde.
# Nur fuer lokale Entwicklung/Debugging explizit aktivieren.
ENABLE_SWAGGER=false
# Laufrolle des Prozesses: api (nur HTTP), worker, job (CLI) oder all (API +
# Worker in einem Prozess, nur fuer lokale Entwicklung und Tests). Ohne Wert
# gilt der Standard des Einstiegspunkts (main: all, worker: worker, cli: job).
# In Produktion Pflicht und "all" ist dort verboten.
# APICORE_ROLE=api
# Port des Health-Endpunkts des Worker-Prozesses.
# WORKER_HEALTH_PORT=3002
# Wetterquelle fuer die Pflanzenentwicklung (Gradtagszahl) und die
# Wetteranzeige. Einzige Quelle ist die freie Open-Meteo-API (bewusst
# uebernommenes Lizenzrisiko, nicht-kommerzielle Bedingungen); der Adapter
# (src/weather/history) erlaubt einen Wechsel. Nur https:-URLs; ein
# unbekannter Quellenname oder eine http:-URL stoppt den Start. Der Schluessel
# ist optional, gehoert in ein Secret und steht nie in Logs oder Fehlermeldungen.
# WEATHER_HISTORY_SOURCE=open-meteo
# OPEN_METEO_FORECAST_URL=https://api.open-meteo.com/v1/forecast
# OPEN_METEO_ARCHIVE_URL=https://archive-api.open-meteo.com/v1/archive
# OPEN_METEO_API_KEY=
# Optional: Kontakt (E-Mail oder URL) fuer den User-Agent-Header der
# Wetteranfragen ("ApiCore/<Version> (+<Kontakt>)"); ohne Wert
# "ApiCore/<Version> (self-hosted beekeeping app)".
# WEATHER_USER_AGENT_CONTACT=
# Telegram-Erinnerungen (optional, T4c). TELEGRAM_BOT_USERNAME (ohne @, 5-32
# Zeichen: Buchstaben, Ziffern, Unterstrich) steht in API und Worker und
# erzeugt den Deep Link. TELEGRAM_BOT_TOKEN (BotFather) gehoert NUR dem Worker,
# im Cluster in das eigene Secret apicore-telegram; der API-Pod darf es nicht
# einbinden. Ein fehlerhaftes Token stoppt nichts: Telegram bleibt aus, der
# Worker loggt einen Fehler (nie den Wert). Bei BotFather Gruppen ausschalten
# (/setjoingroups) und keinen Webhook setzen.
# TELEGRAM_BOT_USERNAME=
# TELEGRAM_BOT_TOKEN=