feat(quick-260923-ad9): Datenmodell, Migration und Reiter-Grundlage - Task 1

Neues Modell Dashboard (D-01/D-02/D-09): position statt Standard-Feld,
kein Unique auf (userId, position) - Umsortieren schreibt spaeter alle
Positionen einer Transaktion neu. WidgetInstance/DashboardLayout haengen
jetzt am Reiter statt am Benutzer (DashboardLayout.dashboardId @unique
ersetzt userId @unique).

Migration 20260923120000_dashboard_tabs: Zeilenschutz mit Mandant- UND
Benutzerdimension (Form 20260911120000/20260921120000), Bestands-
uebernahme fuer jeden Benutzer mit Kacheln oder Anordnung VOR den
Fremdschluesseln (D-03) - gemessen: 0 Kacheln/Anordnungen ohne Reiter,
genau 2 Reiter auf Position 0.

dashboard.service.ts: listDashboards() (Transaktionssperre gegen
doppelte Erstanlage, T-AD9-07), Riegel assertOwnedDashboard() (fail-
closed gegen fremde Reiter, T-AD9-01/02/03) - getLayout/saveLayout/
getWidgets/addWidget laufen jetzt ueber dashboardId statt userId.
GET /dashboard/tabs neu; die vier bestehenden Wege reichen die Reiter-
Kennung durch. Verhalten fuer den Benutzer unveraendert (ein Reiter,
wie bisher) - Task 2 ergaenzt Anlegen/Umbenennen/Loeschen/Umsortieren.

dashboard.service.spec.ts: 43 Tests (31 alte unveraendert + 12 neue fuer
Reiter-Anlage, -Reihenfolge und den Fremdreiter-Riegel bei allen vier
Wegen). Zugriffsklassifikation nachgerechnet: 75 Paare (+1), Bereich
dashboard 21->24 gebunden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 07:56:38 +02:00
parent 84fe73e16a
commit 9c518238f5
9 changed files with 627 additions and 94 deletions
+27 -9
View File
@@ -8,6 +8,7 @@ import {
Patch,
Post,
Put,
Query,
Req,
} from '@nestjs/common';
import type { AuthenticatedRequest } from '../auth/types/auth-user';
@@ -25,10 +26,11 @@ import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto';
* 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
* - GET /dashboard/tabs — list the user's dashboard tabs (quick-260923-ad9)
* - 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
@@ -66,10 +68,23 @@ export class DashboardController {
return { userId: user.id, tenantId, role: user.role };
}
@Get('layout')
async getLayout(@Req() req: AuthenticatedRequest) {
/**
* 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.getLayout(userId, tenantId);
return this.dashboardService.listDashboards(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')
@@ -79,9 +94,12 @@ export class DashboardController {
}
@Get('widgets')
async getWidgets(@Req() req: AuthenticatedRequest) {
async getWidgets(
@Req() req: AuthenticatedRequest,
@Query('dashboardId') dashboardId: string,
) {
const { userId, tenantId, role } = this.extractContext(req);
return this.dashboardService.getWidgets(userId, tenantId, role);
return this.dashboardService.getWidgets(userId, tenantId, role, dashboardId);
}
@Post('widgets')