feat(17-01): move TenderEmailConfig ownership from tenant to user
Alert-Postfach gehoert jetzt dem einzelnen Nutzer (userId @unique) statt
dem Mandanten (D-01) — ein zweiter Kollege desselben Mandanten kann sein
eigenes Postfach anbinden. tenantId bleibt denormalisiert (SMTP-Aufloesung,
Herkunftsmarkierung), wird auf create UND update mitgeschrieben.
- Handgeschriebene Migration (prisma migrate dev verweigert die
nicht-interaktive Shell): befuellt Bestandszeilen mit dem aeltesten
aktiven Administrator ihres Mandanten, entfernt verwaiste Zeilen ohne
Administrator, ersetzt die tenantId-Eindeutigkeit durch userId.
Lokal getestet (0 Bestandszeilen lokal und auf alpha — Zaehlung im
Task-1-Checkpoint), Index-Ergebnis verifiziert.
- TenderEmailConfigService.getConfigForApi/saveConfig auf userId als
Schluessel umgestellt; saveConfig nimmt {userId, tenantId}.
- TendersController: email-config-Routen von @Roles(ADMIN,SUPER_ADMIN)
auf @UseModule('tender-radar') umgestellt (Postfach ist jetzt
Nutzereinstellung); Route-Reihenfolge vor @Get(':id') unveraendert.
- Neue Seite /modules/tender-radar/my-sources ("Meine Quellen") mit dem
unveraenderten EmailAlertConfigForm; Hinweistext benennt D-05 (Tender
bleibt plattform-global — nur wer Quellen einspeist aendert sich).
- tenders.controller.spec.ts an neue Service-Signatur angepasst (Rule 3,
nicht im Plan gelistet, aber zum Kompilieren/Bestehen erforderlich).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,15 +59,17 @@ const DOE_SOURCE_TYPE = 'doe-opendata';
|
||||
* shared platform-wide poll schedule is a platform-admin action, not a
|
||||
* per-tenant module feature.
|
||||
*
|
||||
* Phase 14, Plan 03 (INGEST-05, D-13): `GET`/`PUT /email-config` are, like
|
||||
* `source-config`/`rss-feeds`, per-handler `@Roles(ADMIN, SUPER_ADMIN)`-
|
||||
* guarded — but UNLIKE those (platform-wide singletons/lists), the email
|
||||
* mailbox config is per-TENANT: tenantId is resolved from the auth context,
|
||||
* never the body (T-14-03-05/IDOR). This is also the plan that breaks the
|
||||
* "GET/GET :id are never row-scoped" invariant above, narrowly: `listTenders`/
|
||||
* `getTender` now resolve the requesting tenant to apply the D-13 OR[global,
|
||||
* mine] visibility filter for PRIVATE (email-alert) tenders only — public
|
||||
* tenders (D-03) remain visible to every tenant exactly as before.
|
||||
* Phase 14, Plan 03 (INGEST-05, D-13) originally made `GET`/`PUT
|
||||
* /email-config` per-handler `@Roles(ADMIN, SUPER_ADMIN)`-guarded and
|
||||
* per-TENANT. Phase 17, Plan 01 (D-01) changed this: the mailbox is now
|
||||
* per-USER — every user with module access connects their own inbox, so
|
||||
* these two routes are `@UseModule('tender-radar')`-gated like the other
|
||||
* per-user routes below, and `userId` (not `tenantId`) is the ownership
|
||||
* key, resolved from the auth context, never the body (T-14-03-05/T-17-01/
|
||||
* IDOR). `listTenders`/`getTender` still resolve the requesting tenant to
|
||||
* apply the D-13 OR[global, mine] visibility filter for PRIVATE
|
||||
* (email-alert) tenders — public tenders (D-03) remain visible to every
|
||||
* tenant exactly as before; that part of D-13 is unaffected by D-01.
|
||||
*/
|
||||
@Controller('modules/tender-radar')
|
||||
export class TendersController {
|
||||
@@ -268,37 +270,40 @@ export class TendersController {
|
||||
return this.tenderRssFeedSource.remove(feedId);
|
||||
}
|
||||
|
||||
// ─── E-Mail-Alerts config (Roles-guarded, PER-TENANT, D-06/D-07/D-13) ──────
|
||||
// ─── E-Mail-Alerts config (ModuleGuard-gated, PER-USER, Phase 17 D-01) ─────
|
||||
|
||||
/**
|
||||
* GET /modules/tender-radar/email-config — this tenant's portal-alert
|
||||
* mailbox config (safe-select — never the password, T-07-12). Unlike
|
||||
* `source-config`/`rss-feeds` (platform-wide), this is PER-TENANT:
|
||||
* tenantId is resolved from the auth context, never a query/body field
|
||||
* (T-14-03-05 / V4 — IDOR).
|
||||
* GET /modules/tender-radar/email-config — the requesting USER's own
|
||||
* portal-alert mailbox config (safe-select — never the password,
|
||||
* T-07-12). Phase 17 (D-01): ownership moved from tenant to user — every
|
||||
* user with module access manages their own mailbox, so the Roles guard
|
||||
* (previously ADMIN/SUPER_ADMIN only) is replaced with `@UseModule`, the
|
||||
* same access gate as every other per-user route below. `userId` comes
|
||||
* exclusively from the auth context, never a query/body field
|
||||
* (T-14-03-05 / T-17-01 / V4 — IDOR).
|
||||
*
|
||||
* MUST be declared before `@Get(':id')` below — same route-order pitfall
|
||||
* as `source-config`/`coverage`/`triage`/`rss-feeds`/... above (Pitfall 5).
|
||||
*/
|
||||
@Get('email-config')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
@UseModule('tender-radar')
|
||||
async getEmailConfig(@Req() req: Request) {
|
||||
const { tenantId } = this.extractTriageContext(req);
|
||||
return this.tenderEmailConfig.getConfigForApi(tenantId);
|
||||
const { userId } = this.extractTriageContext(req);
|
||||
return this.tenderEmailConfig.getConfigForApi(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /modules/tender-radar/email-config — upsert this tenant's mailbox
|
||||
* config. tenantId comes exclusively from the auth context — `dto` never
|
||||
* carries a tenantId field (T-14-03-05 / V4 — IDOR). Credential
|
||||
* encrypt-preserve-empty semantics live in TenderEmailConfigService
|
||||
* (mirrors DkvService.saveConfig / T-07-12).
|
||||
* PUT /modules/tender-radar/email-config — upsert the requesting USER's
|
||||
* own mailbox config. userId/tenantId come exclusively from the auth
|
||||
* context — `dto` never carries either field (T-14-03-05 / T-17-01 / V4
|
||||
* — IDOR). Credential encrypt-preserve-empty semantics live in
|
||||
* TenderEmailConfigService (mirrors DkvService.saveConfig / T-07-12).
|
||||
*/
|
||||
@Put('email-config')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
@UseModule('tender-radar')
|
||||
async saveEmailConfig(@Body() dto: TenderEmailConfigDto, @Req() req: Request) {
|
||||
const { tenantId } = this.extractTriageContext(req);
|
||||
return this.tenderEmailConfig.saveConfig(tenantId, dto);
|
||||
const { userId, tenantId } = this.extractTriageContext(req);
|
||||
return this.tenderEmailConfig.saveConfig({ userId, tenantId }, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user