Files
tessera-ctl/apps/api/src/favorites/favorites.controller.ts
T
schalli eebceb298d fix(08): apply code review findings (CR-01, CR-02, WR-01–05, IN-01)
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> to
  isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges
- CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard
  so missing widgetId returns 400 instead of leaking all user favorites
- WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites)
- WR-02: favorites-widget — fix load-path error to use t('favorites.error')
- WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop
- WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain
- WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade
- IN-01: create-widget.dto.ts — update comment from four to eight supported types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 11:03:16 +02:00

90 lines
2.2 KiB
TypeScript

import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
Query,
Req,
} from '@nestjs/common';
import { Request } from 'express';
import { CreateFavoriteDto } from './dto/create-favorite.dto';
import { UpdateFavoriteDto } from './dto/update-favorite.dto';
import { FavoritesService } from './favorites.service';
/**
* REST controller for per-user, per-widget favorite links.
*
* All routes are protected by the global JwtAuthGuard + TenantGuard.
*
* Routes:
* - GET /favorites?widgetId= — list favorites for a widget instance
* - POST /favorites — create a favorite (triggers server-side icon discovery)
* - PATCH /favorites/:id — update a favorite (ownership verified in service)
* - DELETE /favorites/:id — delete a favorite (ownership verified in service)
*/
@Controller('favorites')
export class FavoritesController {
constructor(private readonly favoritesService: FavoritesService) {}
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()
async list(
@Query('widgetId', ParseUUIDPipe) widgetId: string,
@Req() req: Request,
) {
const { userId } = this.extractContext(req);
return this.favoritesService.list(userId, widgetId);
}
@Post()
async create(
@Body() dto: CreateFavoriteDto,
@Req() req: Request,
) {
const { userId, tenantId } = this.extractContext(req);
return this.favoritesService.create(userId, tenantId, dto);
}
@Patch(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateFavoriteDto,
@Req() req: Request,
) {
const { userId } = this.extractContext(req);
return this.favoritesService.update(id, userId, dto);
}
@Delete(':id')
async remove(
@Param('id') id: string,
@Req() req: Request,
) {
const { userId } = this.extractContext(req);
return this.favoritesService.remove(id, userId);
}
}