Files
tessera-ctl/apps/api/src/dashboard/dashboard.controller.ts
T
schalli 67b50240d6 feat(quick-260910-krx): Suchmaschinen gebunden, Katalog begruendet offen, Klassifikation nachgezogen
Aufgabe 3 — TDD zuerst (7 weitere Faelle in dashboard.service.spec.ts, 20
vorher/27 nach dieser Aufgabe), dann die Umstellung:

- getSearchProviders/addSearchProvider/removeSearchProvider laufen ueber
  forTenant(); removeSearchProvider fuehrt Besitzpruefung UND Schreibzugriff
  ueber DENSELBEN gebundenen Klienten. Die drei Vorgabe-Suchmaschinen aus
  der Konstante bleiben unveraendert vorangestellt.
- Der eine Katalogzugriff (this.prisma.module in getWidgets) bleibt
  begruendet ungebunden: Messung und Bedingung getrennt (Tabelle traegt
  heute keinen Zeilenschutz, wirkungslos statt katastrophal — katastrophal
  erst, wenn Etappe 3 eine Regel gibt), unter Berufung auf die bestehende
  Werkzeugpruefung module-tabelle-traegt-keinen-zeilenschutz statt einer
  neuen Behauptung. Ein Wachhund-Testfall haelt den Katalogzugriff aus dem
  Bindungsprotokoll heraus (und beweist zuerst, dass der Katalogpfad
  tatsaechlich durchlaufen wird, nicht nur theoretisch geprueft ist).
- docs/mandantentrennung-zugriffsklassifikation.md an allen fuenf
  handgepflegten Stellen nachgezogen: vier Bestandsaufnahme-Zeilen (inkl.
  eigenstaendiger Nachpruefung der widerlegten SearchProvider-Praemisse),
  Uebersichtszeile (13/0 -> 1/12), Summenzeile (95/147), Klassen-Verteilung
  (unveraendert 63 Paare, ausdruecklich vermerkt), Hintergrunddienst-
  Abschnitt (dashboard hat keinen sechsten Fall, mit Messanweisung), "Was
  diese Etappe NICHT entscheidet" (dienst-interner forTenant()-Weg wie alle
  sieben Bereiche vor ihm).
- .planning/WINDOWS.md traegt Eintrag #25 (offen, Tabelle + JSON): die
  beweisvernichtende Schleife (leeres Dashboard -> Neuaufbau ->
  automatisches Zurueckschreiben -> ueberschriebene Anordnung, Widget-
  Dubletten) samt der Vorabpruefung fuer Etappe 4 und dem Verweis auf #22
  fuer die verwandte Eindeutigkeitsfrage.

Zwei weitere Falsifizierungsnachweise durchgefuehrt: (1) den Katalogzugriff
probeweise gebunden (tenantPrisma.module.findMany) — acht Tests werden rot
mit "TypeError: Cannot read properties of undefined (reading 'findMany')",
weil `module` bewusst nicht in der Testdouble-Bindungsliste steht; Rueckbau
zurueckgenommen, 27/27 wieder gruen. (2) den Stand von dashboardLayout in
der Klassifikationsdatei probeweise auf "ungebunden" gesetzt —
rls-access-inventory.spec.ts wird rot mit "Abweichender Stand (Dokument vs.
Quelltext): ... dokumentiert=ungebunden, gemessen=gebunden"; Ruecknahme,
Testlauf wieder gruen (10/10).

Baseline gehalten: 858 Tests / 56 Dateien gruen, Typpruefung sauber,
Wegwerf-Werkzeug 87/87. Schalter bleibt aus.
2026-09-11 09:03:23 +02:00

127 lines
4.0 KiB
TypeScript

import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
Param,
Patch,
Post,
Put,
Req,
} from '@nestjs/common';
import { Request } from 'express';
import { DashboardService } from './dashboard.service';
import { CreateSearchProviderDto } from './dto/create-search-provider.dto';
import { CreateWidgetDto } from './dto/create-widget.dto';
import { SaveLayoutDto } from './dto/save-layout.dto';
import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto';
/**
* REST controller for dashboard layout and widget instance management.
*
* All endpoints require JWT auth (global JwtAuthGuard).
* Every handler extracts userId and tenantId from the request
* and scopes all operations to the calling user (T-05-01, T-05-02).
*
* Routes:
* - GET /dashboard/layout — get user's saved layout
* - PUT /dashboard/layout — upsert user's layout
* - GET /dashboard/widgets — list user's widget instances
* - POST /dashboard/widgets — create a new widget instance
* - PATCH /dashboard/widgets/:id/config — update widget config
* - DELETE /dashboard/widgets/:id — remove a widget instance
* - GET /dashboard/search-providers — list default + user's custom providers
* - POST /dashboard/search-providers — create a custom search provider
* - DELETE /dashboard/search-providers/:id — remove a custom provider
*/
@Controller('dashboard')
export class DashboardController {
constructor(private readonly dashboardService: DashboardService) {}
private extractContext(req: Request) {
const userId = (req as any).user?.id;
const tenantId =
(req as any).tenantId ?? (req as any).user?.tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
if (!userId) {
throw new ForbiddenException('No user context');
}
return { userId, tenantId };
}
@Get('layout')
async getLayout(@Req() req: Request) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.getLayout(userId, tenantId);
}
@Put('layout')
async saveLayout(@Req() req: Request, @Body() dto: SaveLayoutDto) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.saveLayout(userId, tenantId, dto);
}
@Get('widgets')
async getWidgets(@Req() req: Request) {
const { userId, tenantId } = this.extractContext(req);
const role = (req as any).user?.role;
return this.dashboardService.getWidgets(userId, tenantId, role);
}
@Post('widgets')
async addWidget(@Req() req: Request, @Body() dto: CreateWidgetDto) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.addWidget(userId, tenantId, dto);
}
@Patch('widgets/:id/config')
async updateWidgetConfig(
@Param('id') id: string,
@Req() req: Request,
@Body() dto: UpdateWidgetConfigDto,
) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.updateWidgetConfig(id, userId, tenantId, dto);
}
@Delete('widgets/:id')
async removeWidget(
@Param('id') id: string,
@Req() req: Request,
) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.removeWidget(id, userId, tenantId);
}
// --- Search Providers (05-02, D-15) ---
@Get('search-providers')
async getSearchProviders(@Req() req: Request) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.getSearchProviders(userId, tenantId);
}
@Post('search-providers')
async addSearchProvider(
@Req() req: Request,
@Body() dto: CreateSearchProviderDto,
) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.addSearchProvider(userId, tenantId, dto);
}
@Delete('search-providers/:id')
async removeSearchProvider(
@Param('id') id: string,
@Req() req: Request,
) {
const { userId, tenantId } = this.extractContext(req);
return this.dashboardService.removeSearchProvider(id, userId, tenantId);
}
}