import { Body, Controller, Delete, ForbiddenException, Get, Param, Patch, Post, Put, Query, Req, } from '@nestjs/common'; import type { AuthenticatedRequest } from '../auth/types/auth-user'; import { DashboardService } from './dashboard.service'; import { CreateSearchProviderDto } from './dto/create-search-provider.dto'; import { CreateWidgetDto } from './dto/create-widget.dto'; import { RenameDashboardDto } from './dto/rename-dashboard.dto'; import { ReorderDashboardsDto } from './dto/reorder-dashboards.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/tabs — list the user's dashboard tabs (quick-260923-ad9) * - POST /dashboard/tabs — create a new, empty tab * - PUT /dashboard/tabs/order — persist the tab order (MUST be declared * before the `:id` routes below, see the * source-order guard in dashboard.controller.spec.ts) * - PATCH /dashboard/tabs/:id — rename a tab * - DELETE /dashboard/tabs/:id — delete a tab, its widgets and its layout * - GET /dashboard/layout — get the saved layout of one tab * - PUT /dashboard/layout — upsert the layout of one tab * - GET /dashboard/widgets — list the widget instances of one tab * - POST /dashboard/widgets — create a new widget instance on one tab * - 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) {} /** * BEFUND quick-260921-m34 (D-03, gemeldet nicht repariert): `getWidgets` * las die Rolle vorher als `req.user?.role` NACH dieser Pruefung und gab * sie an `DashboardService.getWidgets(role: Role)` weiter, das eine Rolle * zwingend verlangt. Der Bestandscode nahm also an, dass an dieser Stelle * immer ein Aufrufer vorliegt. Die Annahme stimmt — die Pruefung "No user * context" direkt darunter erzwingt sie seit jeher —, aber der Compiler * konnte die beiden Stellen nicht verbinden, weil sie in zwei Methoden * standen. Deshalb gibt diese Methode die Rolle jetzt MIT zurueck: keine * neue Pruefung, kein erfundener Wert, gleiche Reihenfolge, gleiche * Meldungen, gleiches Verhalten — nur sichtbar statt angenommen. */ private extractContext(req: AuthenticatedRequest) { const user = req.user; const tenantId = req.tenantId ?? user?.tenantId; if (!tenantId) { throw new ForbiddenException('No tenant context'); } if (!user?.id) { throw new ForbiddenException('No user context'); } return { userId: user.id, tenantId, role: user.role }; } /** * Reiter des Benutzers (quick-260923-ad9), nach Position aufsteigend; * legt beim ersten Aufruf genau einen an. */ @Get('tabs') async listDashboards(@Req() req: AuthenticatedRequest) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.listDashboards(userId, tenantId); } @Post('tabs') async createDashboard(@Req() req: AuthenticatedRequest) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.createDashboard(userId, tenantId); } /** * MUSS vor `PATCH tabs/:id` / `DELETE tabs/:id` stehen — in dieser * Anwendung hat eine Route mit Platzhalter schon einmal eine dahinter * stehende feste Route verdeckt (siehe Projektnotiz „NestJS Route- * Order“); ein quelltextlesender Wächter in * `dashboard.controller.spec.ts` prüft die Reihenfolge im Dateitext. */ @Put('tabs/order') async reorderDashboards( @Req() req: AuthenticatedRequest, @Body() dto: ReorderDashboardsDto, ) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.reorderDashboards(userId, tenantId, dto); } @Patch('tabs/:id') async renameDashboard( @Param('id') id: string, @Req() req: AuthenticatedRequest, @Body() dto: RenameDashboardDto, ) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.renameDashboard(id, userId, tenantId, dto); } @Delete('tabs/:id') async deleteDashboard(@Param('id') id: string, @Req() req: AuthenticatedRequest) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.deleteDashboard(id, userId, tenantId); } @Get('layout') async getLayout( @Req() req: AuthenticatedRequest, @Query('dashboardId') dashboardId: string, ) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.getLayout(userId, tenantId, dashboardId); } @Put('layout') async saveLayout(@Req() req: AuthenticatedRequest, @Body() dto: SaveLayoutDto) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.saveLayout(userId, tenantId, dto); } @Get('widgets') async getWidgets( @Req() req: AuthenticatedRequest, @Query('dashboardId') dashboardId: string, ) { const { userId, tenantId, role } = this.extractContext(req); return this.dashboardService.getWidgets(userId, tenantId, role, dashboardId); } @Post('widgets') async addWidget(@Req() req: AuthenticatedRequest, @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: AuthenticatedRequest, @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: AuthenticatedRequest, ) { 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: AuthenticatedRequest) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.getSearchProviders(userId, tenantId); } @Post('search-providers') async addSearchProvider( @Req() req: AuthenticatedRequest, @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: AuthenticatedRequest, ) { const { userId, tenantId } = this.extractContext(req); return this.dashboardService.removeSearchProvider(id, userId, tenantId); } }