feat(08-03): FavoriteLink schema + FavoritesModule (CRUD + SSRF icon discovery)
- Add FavoriteLink Prisma model (userId/tenantId/widgetId scope, iconUrl nullable, position) - IconDiscoveryService: port SSRF-protected icon discovery with redirect: 'manual', private IP / blocked-hostname checks, 4000ms timeout, 200k HTML cap (T-08-05) - FavoritesService: list/create/update/remove all scoped by userId (T-08-06 / Pitfall 3) - FavoritesController: GET /favorites?widgetId, POST, PATCH :id, DELETE :id - FavoritesModule registered in AppModule - tsc --noEmit passes for @tessera/api
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
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') 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user