(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.
Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.
BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.
BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).
Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.
noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
buildTenderWhere gains an optional ownerTenantId param: a resolved
requesting tenant sees global tenders (null) plus its own private ones
(OR[global, mine]); an unresolved requester fails CLOSED to global-only —
never an accidental cross-tenant leak.
TendersController: listTenders/getTender resolve the requesting tenant
leniently from the auth context (resolveRequestingTenantId, never throws)
and apply the D-13 filter; getTender 404s (not a distinct "forbidden") when
a tender's non-null ownerTenantId doesn't match the requester, so no
cross-tenant detail leak. New GET/PUT /modules/tender-radar/email-config
routes (Roles ADMIN/SUPER_ADMIN, tenantId from auth context, never the
body) delegate to TenderEmailConfigService — declared before @Get(':id')
per the project's NestJS route-order convention.
Web: EmailAlertConfig type + fetchEmailConfig/saveEmailConfig client
functions; EmailAlertConfigForm mirrors the DKV InboxConfigForm (password
blank on load, only sent when typed — T-07-12), added as a new
"E-Mail-Alerts" section on the existing tender-radar settings page.
Hardcoded German strings — i18n is Plan 14-05.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the batch-triage read/write routes (declared before @Get(':id') per
the route-order pitfall, T-11-13) and wires them through
TenderTriageService with userId/tenantId always derived from the request
context, never the body (T-11-10 / V4 IDOR). Extends TenderQueryDto/
buildTenderWhere with favOnly (UI-04): the controller resolves the
current user's favorited tenderIds server-side before building the
where-clause, and an empty favorites list yields zero matches instead of
the unfiltered catalog. Both batch-ids and favIds in-lists are bounded
(T-11-11 DoS). tenders.controller.spec.ts constructor calls updated for
the new TenderTriageService dependency (Rule 3 — required to keep the
existing suite compiling/passing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TenderQueryDto gains a validated cpv[] field (single-or-repeated query
param, normalized via @Transform); buildTenderWhere adds a cpvDivisions
hasSome branch (FILTER-03, Pitfall 2 — never an exact match against raw
cpvCodes). FilterPanel gets a CPV-Division autocomplete (search-by-label,
multi-select chips, repeated ?cpv= params) plus the previously
backend-only value filter's UI: valueMin/valueMax number inputs and an
"ohne Wertangabe einschließen" toggle (default on, matches the builder's
includeNullValue default from Plan 11-01) so the 91.6% NULL-value rows
stay visible by default. Verified against the live DB: cpv=45 matches all
three raw formats ("45", "45000000", "45000000-7") via the backfilled
cpvDivisions column.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TenderQueryDto gains validated plz (@MaxLength(5)), region, and
bundesland fields. buildTenderWhere adds three conditional AND-branches:
plz startsWith, bundesland exact-match against the now-backfilled indexed
column (Pitfall 1), and region startsWith (usable independent of the
bundesland column). FilterPanel gets a PLZ input and a 16-Land Bundesland
dropdown (mirrors NUTS1_BUNDESLAND — web/api are separate packages) that
write plz/bundesland into the URL searchParams; ResultsList already
forwards the full URLSearchParams to listTenders(), so no additional
fetch wiring was needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildTenderWhere: conditional Prisma where-builder covering keyword
(D-01), openOnly deadline default + explicit deadline range (D-04),
and NULL-graceful value filter (D-05, Kern-Test: value filter never
eliminates estimatedValue=null rows). buildOrderBy: sort whitelist
(deadline/value/published) defaulting to publishedAt desc (UI-01,
T-11-01 — no dynamic orderBy keys from user input).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>