df7a5e7e8e
dashboard.service.ts: createDashboard() (Namensvergabe "Dashboard N" fuellt Luecken, D-08; Obergrenze 20 Reiter, T-AD9-06), renameDashboard() (Riegel zuerst), deleteDashboard() (letzter Reiter bleibt, D-10; Loeschen + Neu- Nummerierung als EINE withTenantTransaction), reorderDashboards() (woertlich nach FavoritesService.reorder-Muster: Exakt-Abgleich vor jedem Schreiben, kein Teilschreiben, dieselbe Abweisung fuer unvollstaendige/unbekannte/ fremde Kennungen - T-AD9-04). dashboard.controller.ts: fuenf neue Wege unter tabs; PUT tabs/order VOR PATCH/DELETE tabs/:id deklariert (Routenreihenfolge). dashboard.controller.spec.ts (neu, 8 Tests): Durchreichung, Abweisung ohne Kontext, quelltextlesender Waechter fuer die Routenreihenfolge. dashboard.service.spec.ts: 61 Tests (43 alte + 18 neue fuer Anlegen, Umbenennen inkl. DTO-Beschneidung/-Laengenpruefung, Loeschen und Umsortieren - je ein Fall fuer "fremder Reiter" und "letzter Reiter bleibt"). Deviation (Rule 1): widget-module-map.spec.ts's CreateWidgetDto-Whitelist helper needed a dashboardId fixture after Task 1 made the field required - fixed inline, out of the plan's files_modified list but directly caused by Task 1's DTO change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
202 lines
7.4 KiB
TypeScript
202 lines
7.4 KiB
TypeScript
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);
|
|
}
|
|
}
|