feat(05-01): dashboard backend — Prisma models, CRUD API, module wiring

- Add DashboardLayout and WidgetInstance Prisma models with userId/tenantId scoping
- Create DashboardController with 6 endpoints (layout CRUD + widget CRUD)
- Create DashboardService with ownership verification on all widget mutations (T-05-01)
- Add SaveLayoutDto, CreateWidgetDto, UpdateWidgetConfigDto with class-validator
- Register DashboardModule in app.module.ts imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 11:14:03 +02:00
parent f925fd3654
commit 950eebbc15
8 changed files with 306 additions and 0 deletions
@@ -0,0 +1,95 @@
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
Param,
Patch,
Post,
Put,
Req,
} from '@nestjs/common';
import { Request } from 'express';
import { DashboardService } from './dashboard.service';
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
*/
@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 } = this.extractContext(req);
return this.dashboardService.getLayout(userId);
}
@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 } = this.extractContext(req);
return this.dashboardService.getWidgets(userId);
}
@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 } = this.extractContext(req);
return this.dashboardService.updateWidgetConfig(id, userId, dto);
}
@Delete('widgets/:id')
async removeWidget(
@Param('id') id: string,
@Req() req: Request,
) {
const { userId } = this.extractContext(req);
return this.dashboardService.removeWidget(id, userId);
}
}