feat(api,web): eigene Module — Tabelle, API, Seitenleiste, Rahmen-Seite

- Tabelle CustomModule mit Zeilenschutz (tenant_isolation_policy), Migration 20260929120000
- API /custom-modules: Lesen für jeden Angemeldeten, Schreiben nur Administrator, nur https ohne Zugangsdaten
- Seitenleiste zeigt eigene Module unter ihrer Kategorie, Rahmen-Seite mit Sandbox und „In neuem Tab öffnen“
- MODULE_CATEGORIES als gemeinsame Liste, Zugriffsklassifikation nachgemessen fortgeschrieben

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 07:25:33 +02:00
parent 643b1a2caa
commit b9d87be360
23 changed files with 1270 additions and 40 deletions
@@ -0,0 +1,73 @@
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
Param,
Patch,
Post,
Req,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { Roles } from '../auth/decorators/roles.decorator';
import type { AuthenticatedRequest } from '../auth/types/auth-user';
import { CustomModulesService } from './custom-modules.service';
import { CreateCustomModuleDto, UpdateCustomModuleDto } from './dto/custom-module.dto';
/**
* Eigene Module (quick-260929-9wc). Lesen (`GET`, `GET :id`) steht jedem
* angemeldeten Benutzer offen — die Eintraege sind fuer alle sichtbar (D-01);
* Schreiben nur `@Roles(ADMIN, SUPER_ADMIN)` (T-9WC-01). Kein `@UseModule`:
* eigene Module haengen an keiner Modul-Aktivierung. `tenantId` kommt
* ausschliesslich aus `req.tenantId` (gesetzt vom `TenantGuard`).
*
* ROUTEN-REIHENFOLGE: NestJS bildet Routen in Deklarationsreihenfolge ab.
* Jede kuenftige statische GET-Route MUSS ueber `getOne` (`@Get(':id')`)
* stehen, sonst faengt `:id` sie ab (404-Shadowing); der Controller-Test
* haelt die Reihenfolge von `list` vor `getOne` fest.
*/
@Controller('custom-modules')
export class CustomModulesController {
constructor(private readonly service: CustomModulesService) {}
private requireTenantId(req: AuthenticatedRequest): string {
const tenantId = req.tenantId;
if (!tenantId) {
throw new ForbiddenException('Kein Mandantenkontext');
}
return tenantId;
}
@Get()
async list(@Req() req: AuthenticatedRequest) {
return this.service.list(this.requireTenantId(req));
}
@Get(':id')
async getOne(@Req() req: AuthenticatedRequest, @Param('id') id: string) {
return this.service.getOne(this.requireTenantId(req), id);
}
@Post()
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateCustomModuleDto) {
return this.service.create(this.requireTenantId(req), dto);
}
@Patch(':id')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async update(
@Req() req: AuthenticatedRequest,
@Param('id') id: string,
@Body() dto: UpdateCustomModuleDto,
) {
return this.service.update(this.requireTenantId(req), id, dto);
}
@Delete(':id')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async remove(@Req() req: AuthenticatedRequest, @Param('id') id: string) {
return this.service.remove(this.requireTenantId(req), id);
}
}