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:
@@ -1,4 +1,10 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateFavoriteDto } from './dto/create-favorite.dto';
|
||||
import { UpdateFavoriteDto } from './dto/update-favorite.dto';
|
||||
@@ -94,4 +100,32 @@ export class FavoritesService {
|
||||
|
||||
await this.prisma.favoriteLink.delete({ where: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the raw bytes of a favorite's stored icon, scoped to the
|
||||
* requesting user (T-08-06 — same ownership check as update/remove).
|
||||
* Never accepts a client-supplied URL — only the stored iconUrl on a
|
||||
* row the caller owns is fetched (T-QFIP-01).
|
||||
*
|
||||
* Throws NotFoundException (404) if the row doesn't exist, isn't owned
|
||||
* by the caller, or has no icon on record. Throws a 502 HttpException
|
||||
* if the upstream fetch fails (unreachable, timeout, non-image, or
|
||||
* SSRF-blocked) -- never returns a placeholder image.
|
||||
*/
|
||||
async getIconBytes(
|
||||
id: string,
|
||||
userId: string,
|
||||
): Promise<{ contentType: string; body: Buffer }> {
|
||||
const link = await this.prisma.favoriteLink.findUnique({ where: { id } });
|
||||
|
||||
if (!link || link.userId !== userId || !link.iconUrl) {
|
||||
throw new NotFoundException('FavoriteLink not found');
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.iconDiscovery.fetchIconBytes(link.iconUrl);
|
||||
} catch {
|
||||
throw new HttpException('Icon fetch failed', HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user