feat(favorites): GET /favorites/:id/icon proxy endpoint
Ownership-scoped (userId, matching update/remove) icon byte proxy. Loads the row's stored iconUrl server-side and streams it through IconDiscoveryService.fetchIconBytes -- never accepts a client-supplied URL, so this can't become an open SSRF proxy. Not-found/not-owned/no-icon -> 404. Upstream fetch failure (unreachable, timeout, non-image, SSRF-blocked) -> 502, never a 200 with a placeholder. Success sets Cache-Control so the browser doesn't refetch every load. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,9 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { Request, Response } from 'express';
|
||||
import { CreateFavoriteDto } from './dto/create-favorite.dto';
|
||||
import { UpdateFavoriteDto } from './dto/update-favorite.dto';
|
||||
import { FavoritesService } from './favorites.service';
|
||||
@@ -66,6 +67,32 @@ export class FavoritesController {
|
||||
return this.favoritesService.create(userId, tenantId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /favorites/:id/icon — streams the stored icon bytes for a favorite
|
||||
* owned by the caller, from Tessera's own origin. This avoids the browser
|
||||
* blocking a cross-origin <img> hotlink when the external site sends
|
||||
* Cross-Origin-Resource-Policy: same-origin (e.g. claude.ai).
|
||||
*
|
||||
* Takes only a FavoriteLink id — never a client-supplied URL — so this
|
||||
* cannot be used as an arbitrary-URL SSRF proxy (T-QFIP-01).
|
||||
*/
|
||||
@Get(':id/icon')
|
||||
async getIcon(
|
||||
@Param('id') id: string,
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { userId } = this.extractContext(req);
|
||||
const { contentType, body } = await this.favoritesService.getIconBytes(
|
||||
id,
|
||||
userId,
|
||||
);
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.send(body);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
|
||||
Reference in New Issue
Block a user