Global admin-managed RSS feed list (TenderRssFeedSource, D-08/D-14) with
a save-time hostname/SSRF guard (TenderRssFeedSourceService) — RSS feed
URLs are runtime admin input, so the code-level SourceRegistry denylist
gate does not cover them; a separate check rejects DENYLISTED_PORTALS
hostnames, non-http(s) schemes, and private/loopback hosts.
Adds TenderSourcePollConfig.pollGranularity ('day' | 'tick', D-15):
pollDueSources() branches per source — 'day' sources keep the existing
lastIngestedDay gate byte-unchanged, 'tick' sources (rss) fetch on every
active scheduler tick regardless of lastIngestedDay, since the day-cursor
gate was built for a genuine daily batch-export API and would otherwise
silently cap RSS to one fetch per calendar day.
Wires RssAdapter.fetchTenders() to fan out over active feed rows (native
fetch + AbortController 15s + response-size ceiling, catch-per-feed),
registers it in tenders.module.ts, and seeds the 'rss' poll config
active with pollGranularity='tick' plus a default-active service.bund.de
feed row (subreport-elvis has no single canonical URL — zero rows seeded,
admin adds relevant municipality feeds).
Migration applied locally per project convention (host -> container IP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Additive schema change (SCHEMA-03/D-03/D-04): new model TenderSource
(1:n Tender, @@unique[sourcePortal, sourceNoticeId], onDelete Cascade)
and a nullable Tender.fingerprint column + index. dedupKey stays
unchanged as the SCHEMA-02 upsert target.
Migration 20260723120000_add_tender_source applies in strict order
(Pitfall 5): table+column create, then one TenderSource row per
pre-existing Tender via SQL INSERT/SELECT, then the unique constraint.
Applied locally against the tessera dev DB (container IP, no host
port) — verified via psql: TenderSource count == Tender count == 2851.
backfill-tender-source.ts is a one-time script that computes
Tender.fingerprint via the Task-1 tenderFingerprint() function
(Decimal->number conversion for estimatedValue, T-13-01-03) — run via
the compiled dist/ output (source uses standard extensionless TS
imports for tsc compatibility). Confirmed: 2851/2851 rows backfilled,
idempotent re-run verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-tenant "Skip TLS certificate verification" toggle to the LDAP
admin page so admins can connect to an AD whose ldaps:// certificate is
signed by an internal/self-signed CA (Node error: "unable to verify the
first certificate"). When enabled, ldapts is given
tlsOptions.rejectUnauthorized=false; the flag is ignored for plain ldap://
(no TLS). Defaults to full verification.
New Boolean column LdapConfig.tlsRejectUnauthorized (@default(true)) +
migration; wired through DTOs, config service, all Client creations
(test/groups/user-search/import/sync) and the test-connection endpoint. UI
checkbox with an insecure-network warning (de/en). 3 new service specs;
API 218 green, web 131 green, both apps tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TenderMatch: one row per (tenderId, savedSearchId) pair, single nullable
notifiedAt as the matched-vs-notified eligibility gate (D-06)
- TenderNotificationPref: per-user digest interval (daily/weekly/off, D-01/D-03)
- TenderSavedSearch.instantAlert: per-profile instant alert flag, default off (D-04)
- Migration 20260722100000_add_tender_notifications applied to local dev DB
(docker exec psql), recorded in _prisma_migrations, prisma generate run
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GREEN phase — adds TenderSavedSearch (userId+tenantId scoped, filters
Json, @@unique([userId,name])), the migration (applied to local dev DB),
and TenderSavedSearchService following the FavoritesService/
TenderTriageService pattern: manual where:{userId} scoping (no
forTenant()/RLS), ownership check before update/remove, P2002 unique
conflicts translated to ConflictException.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RED phase (TDD) for UI-03/04 per-user triage (gelesen/ungelesen, Favorit).
Adds the TenderTriage Prisma model (userId-scoped, onDelete: Cascade to
Tender per Pitfall 6) and its migration, plus a failing spec proving
upsert idempotency, strict userId scoping (V4/IDOR, T-11-10), and cascade
consistency — service implementation follows in the GREEN commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds Tender.cpvDivisions String[] (@@index Gin) derived at ingestion time
via cpv-catalog.ts's divisionOf() — replaces exact-match cpvCodes
comparison with a typesafe, GIN-indexable hasSome target (FILTER-03,
Pitfall 2). Backfill migration 20260721150000_tender_cpv_divisions_backfill
applied locally: 1612/1671 rows populated across all observed divisions
(741 rows carry division '45' — Bauarbeiten); idempotent (second run:
UPDATE 0, ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS both skip
cleanly). Applied via docker exec psql + `prisma migrate resolve
--applied` + `prisma generate` against the local dev DB only — no
Docker deploy on the test server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the Phase-10-deferred `bundesland = null` assignment with
bundeslandFromRegion(region) so new ingests are Bundesland-filterable
immediately. Add @@index([bundesland]) for filter performance. New
handwritten migration 20260721140000_tender_bundesland_backfill backfills
the ~1671 pre-existing rows (idempotent UPDATE, only where bundesland IS
NULL AND region IS NOT NULL) — applied locally via
`docker exec tessera-ctl-db-1 psql`, resolved as applied in
_prisma_migrations, and prisma generate re-run.
Verified on the local dev DB: 933/1671 rows now have bundesland set
across all 16 Länder (738 remain NULL where region itself is NULL).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-username denylist so individual accounts (service accounts like
administrator, krbtgt, guest, ldap$) can be excluded from LDAP sync,
independent of the group/OU include-filter which only scopes the search.
- schema: LdapConfig.userExcludeList String[] (+ migration)
- sync: skip excluded usernames (case-insensitive) before recording the DN,
so an already-imported user added to the list gets deactivated next sync
- DTO / config service / controller / scheduler: thread userExcludeList through
- web: exclude-list admin UI section (add/remove/save) + de/en translations
- tests: 3 specs covering empty list, case-insensitive skip, deactivation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bindDn and bindPassword are now optional on LdapConfig (nullable
migration) and throughout the DTOs/service/client -- an admin can
leave both blank to connect to directories that permit anonymous
read access. LdapService.bind() falls back to an RFC 4513 anonymous
bind (empty DN + empty password) whenever either field is missing,
shared across testConnection, listGroups, and syncUsersForTenant.
Frontend: removed the required attribute from Bind-DN/Bind-Passwort,
added a placeholder hint ("leer = anonymous bind"), and the
"Verbindung testen" button now only needs a Server-URL to enable
(not bindDn+bindPassword). Config responses now return bindPassword
as null (not a misleading "********") when no password is set.
Verified locally: submitted only a Server-URL with both bind fields
empty and confirmed the request reached the anonymous-bind code path
(DNS failure for the unreachable test host, not a validation error).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Persists per-tenant AD group/OU DNs to restrict which directory
entries get synced. Empty array (default) preserves current
behavior — import everyone under base DN.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Prisma: domain String? added to CalendarSource model (db push applied)
- DTOs: domain in CreateCalendarSourceDto, UpdateCalendarSourceDto, new TestCalendarSourceConfigDto
- Service: domain in SOURCE_SAFE_SELECT, addSource, updateSource; new testConnectionFromConfig method
- Controller: POST /calendar/sources/test-config (before :id routes to avoid collision)
- ExchangeProvider: domain in all source interfaces; passed as 3rd arg to EWS WebCredentials
- Frontend: domain in CalendarSource/CreateSourcePayload/UpdateSourcePayload; testSourceConfig API fn
- Form: domain field (Exchange-only), "Test connection" button with idle/loading/success/error states
- i18n: de+en keys for formFieldDomain, formFieldDomainHint, formTestConnection, formTesting, formTestSuccess, formTestFailed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> to
isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges
- CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard
so missing widgetId returns 400 instead of leaking all user favorites
- WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites)
- WR-02: favorites-widget — fix load-path error to use t('favorites.error')
- WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop
- WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain
- WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade
- IN-01: create-widget.dto.ts — update comment from four to eight supported types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add avatarPath String? column to User model (migration: add_user_avatar)
- POST /users/me/avatar: 2MB limit, image/png/jpeg/webp allowlist, writes to user-files/avatars/{userId}.{ext}
- GET /users/me/avatar: streams avatar with Cache-Control: no-store
- AuthService.getMe(): returns isLocalUser + hasAvatar without leaking passwordHash/ldapDn
- AuthController GET /auth/me: now returns enriched profile via getMe()
- Prisma model SearchProvider with userId/tenantId scoping
- Three default providers (Google/Bing/DuckDuckGo) as constants, always returned without DB seed
- GET/POST/DELETE search-providers endpoints on DashboardController
- Ownership verification on delete (T-05-07), default providers cannot be deleted
- CreateSearchProviderDto with class-validator: urlTemplate must contain {query} (T-05-08)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NestJS app with ConfigModule and HealthModule
- GET /health endpoint returning {status, timestamp} using HealthResponse type
- Prisma schema with PostgreSQL datasource and Tenant model (multi-tenancy foundation)
- Multi-stage Dockerfile with non-root nestjs user, monorepo root as build context
- Workspace dependency on @tessera/shared for shared types
- Type-check passes successfully