One Entra enterprise application now covers both interactive login and
the calendar sync's client-credentials Graph access, instead of
requiring a second app registration. GraphClientService reads
OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET (same as SSO) and
derives the tenant ID from the issuer URL's path, rather than three
dedicated M365_CALENDAR_* env vars. The app registration just needs the
"Calendars.ReadWrite" Application permission added alongside its
existing delegated login scopes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds a one-way push (ApiCore -> M365) of Aufgaben/Durchsichten/
Behandlungen/Fütterungen/Ernten/Königinnen-Zuordnungen into the calendar
of a Microsoft 365 group, via the Graph API.
- CalendarSyncModule (app-only client-credentials Graph auth, separate
from the interactive SSO app registration; new env vars
M365_CALENDAR_TENANT_ID/_CLIENT_ID/_CLIENT_SECRET).
- Outbox + minute-interval cron worker (CalendarSyncOutboxEntry) instead
of a synchronous push, since Graph calls are too slow/flaky to block a
domain create request on; failed syncs retry up to 5 times.
- Per-source-type toggle plus a group-ID field, configurable in
Verwaltung > Automatisierung > Kalender-Sync (Verwaltung > Backup, an
env var still wins on those fields where one applies to related config).
- Hooked into Tasks/Inspections/Treatments/Feedings/Harvests/Queens at
their existing create (and Task update/complete) points; entries render
as all-day Graph events to avoid timezone ambiguity.
- Documents the design decision in ARCHITECTURE.md (Annahme 9), since
this is a net-new capability the lastenheft doesn't scope.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Backup export/import now covers Role, User, RolePermission,
Notification, AuditLog and Document (with actual file content), not
just business data - matching "wirklich alle Daten". Role/User use
natural keys (role.key, resource+action) so a bundle restores cleanly
into a different database.
Adds BackupSchedulerService (@nestjs/schedule) for cron-based automatic
backups to a local path (BACKUP_STORAGE_PATH), with retention pruning
and a file-mtime guard against duplicate runs across replicas. New
GET /backup/schedule and POST /backup/schedule/run-now endpoints, plus
a status card and manual trigger in the frontend Backup settings page.
JWT_ACCESS_SECRET war zwar als Pflicht-Env-Var geprueft, aber ohne
Mindestlaenge - ein kurzer/erratbarer Wert haette nicht nur das
Faelschen von Access-/MFA-/SSO-Tokens fuer jeden Nutzer erlaubt,
sondern auch (ueber mfa-crypto.util.ts, das den AES-Schluessel fuer
gespeicherte TOTP-Secrets aus genau diesem Wert ableitet) das
Entschluesseln aller gespeicherten MFA-Secrets.
main.ts bricht den Start jetzt mit klarer Fehlermeldung ab, wenn
JWT_ACCESS_SECRET kuerzer als 32 Zeichen ist. .env.example und das
K8s-Secret-Template zeigen jetzt einen ausreichend langen Platzhalter
mit Erzeugungshinweis (openssl rand -hex 32).
Betrifft nur main.ts's bootstrap() - e2e-Tests bootstrappen ueber
test/utils/create-test-app.ts direkt via configureApp() und durchlaufen
diese Pruefung nicht, daher keine Testanpassung noetig.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Kein separater Kubernetes-Job mehr. Dockerfile-CMD fuehrt jetzt
`npm run migrate:deploy` aus, bevor `node dist/main.js` startet -
Tabellen werden beim ersten Verbinden automatisch angelegt. Sicher bei
mehreren gleichzeitig startenden Pod-Replicas (replicas: 2), da Prisma
migrate deploy einen DB-seitigen Advisory-Lock nutzt. deployment.yaml
bekommt eine startupProbe (failureThreshold 30 x periodSeconds 10 = bis
zu 5 Min), damit liveness/readiness den Container waehrend der
Migration nicht neu starten. ARCHITECTURE.md entsprechend aktualisiert,
migrate-job.yaml entfernt.
Bisher lief keine Migration automatisch: dem Runtime-Image fehlten
Prisma-CLI, prisma/migrations/ und prisma.config.ts (nur dist+generated
wurden kopiert, prisma war devDependency). Jetzt:
- prisma von devDependencies zu dependencies verschoben (wird fuer
migrate:deploy im --omit=dev Runtime-Image gebraucht)
- neues npm-Script migrate:deploy (prisma migrate deploy)
- Dockerfile kopiert prisma/, prisma.config.ts zusaetzlich ins Runtime-Image
- deploy/k8s/backend/migrate-job.yaml: Job, der dasselbe Backend-Image mit
ueberschriebenem command nutzt statt eines eigenen Migrations-Images,
laeuft einmalig vor dem Rollout (Reihenfolge/kubectl-Befehle als
Kommentar in der Datei)
Verifiziert: tsc, 253/253 Unit-Tests, Build clean, `npx prisma --version`
laedt prisma.config.ts korrekt.
Architektur-Entscheidung geaendert: PostgreSQL laeuft jetzt selbst im
Cluster statt auf einer bestehenden externen Instanz. Neu:
deploy/k8s/postgres/ mit StatefulSet (postgres:18-alpine,
volumeClaimTemplates, pg_isready-Probes), headless Service
(apicore-postgres) und Secret-Vorlage. Backend-DATABASE_URL zeigt auf
den neuen In-Cluster-Service. CLAUDE.md/ARCHITECTURE.md aktualisiert:
Backup-Verantwortung liegt jetzt beim Projekt, nicht mehr beim
externen Cluster-/DBA-Betrieb (Backup-CronJob als offener Punkt in
TASKS.md ergaenzt).
runs-on ubuntu-latest passte auf keinen registrierten Runner (Label
docker-builder erforderlich, siehe funktionierendes Referenzprojekt).
Push laeuft jetzt ueber den cluster-internen Service gitea:80 statt
git.encry.de, da der Cloudflare-Proxy Registry-Blob-Uploads mit "413
Payload Too Large" ablehnt. Auth ueber gitea.actor + REGISTRY_TOKEN
(GITEA_TOKEN hat kein Package-Write-Recht). Image-Pfad wird jetzt
kleingeschrieben (OCI verbietet Grossbuchstaben) - betrifft auch den
Owner-Namen in den K8s-Deployment-Manifesten.
Verified each finding against the real codebase first, since the review's
diff was polluted by a stale leftover agent-worktree copy under
.claude/worktrees/ (now removed) that made several already-fixed or
never-broken issues look current (CORS, missing typecheck script, PWA
icons, font CSS, Helmet/Swagger CSP - all confirmed fine live, not
touched).
Real fixes:
- main.ts: fail-fast on missing DATABASE_URL/JWT_ACCESS_SECRET at boot,
call enableShutdownHooks() so PrismaService.onModuleDestroy() actually
runs on SIGTERM during k8s rolling deploys, PORT fallback 3000 -> 3001
to match every other place that documents the port.
- health.controller.ts: log the real underlying DB error instead of
discarding it, add a 5s timeout to the readiness query so a hung DB
fails the probe fast instead of blocking indefinitely.
- Frontend Dockerfile: declare ARG/ENV NEXT_PUBLIC_API_URL before the
build step, since Next.js inlines NEXT_PUBLIC_* vars at build time and
a runtime-only ConfigMap value never reaches the compiled bundle.
- Backend ingress.yaml: drop the no-op rewrite-target regex for a plain
prefix path (identical behavior, less confusing).
Security review findings (HIGH-1/2/3, MEDIUM):
- Rate limiting via @nestjs/throttler: global 100 req/min default, stricter
10 req/min on login and refresh. Also closes the MEDIUM finding (public
batch-lookup enumeration) since it now sits behind the global limit too.
Trust the K8s ingress's single proxy hop so throttling keys off the real
client IP, not the ingress pod's IP.
- Auth events (login success/failure, logout, token refresh) are now
audit-logged via new AuditAction values, closing the gap where the
project's own audit-log principle never actually covered the Auth module.
- Refresh-token cookie now defaults to Secure=true (opt out only via
COOKIE_SECURE=false for local HTTP dev), instead of keying off NODE_ENV
which was never set in the K8s deploy templates. Added a TLS block +
cert-manager annotation to both the backend and frontend ingress
templates, which previously had no HTTPS termination configured at all.
/login: Formular gegen AuthProvider (Access-Token im Memory, Refresh
per httpOnly-Cookie), Fehlermeldung bei falschen Zugangsdaten.
(app)/layout.tsx: geschuetztes Layout, leitet ohne Session zu /login um,
Header mit Nutzer-E-Mail + Abmelden. settings/layout.tsx: zusaetzliches
Admin-Gate fuer /settings/users.
/settings/users: Liste, Anlegen-Dialog (shadcn/Base UI Dialog+Select),
Deaktivieren - alles ueber TanStack Query gegen die Users-API.
proxy.ts (Next 16 Nachfolger von middleware.ts): optimistische
Cookie-Praesenz-Pruefung fuer /login und /settings/*, echte
Autorisierung bleibt serverseitig (RolesGuard).
Backend-Fixes waehrend Browser-Verifikation gefunden und behoben:
- CORS fehlte komplett (app.enableCors, FRONTEND_ORIGIN) - jeder
Cross-Origin-Request von :3000 nach :3001 wurde geblockt.
- React-StrictMode-Doppel-Effect liess zwei parallele Refresh-Aufrufe
laufen; da Refresh-Tokens rotierend/einmalig sind, gewann einer und
der andere bekam 401 -> Nutzer flog nach jedem Reload aus der Session.
Fix: AbortController im Session-Restore-Effect.
- Manifest verwies auf nicht existente PNG-Icons (404); durch
public/icon.svg ersetzt.
7 Vitest-Tests (Login-Formular, Users-Liste, JWT-Decode), Lint,
Typecheck und Build gruen. Kompletter Flow (Login, Reload-Persistenz,
Benutzer anlegen/deaktivieren, Abmelden, Redirect ohne Session) live
im Browser gegen den echten Server verifiziert.