From 7500e1e971f03720fb788c9d5366b333e61b2a08 Mon Sep 17 00:00:00 2001 From: AMDYFX Date: Sun, 18 Oct 2026 16:48:13 +0100 Subject: [PATCH 1/4] fix(server): don't trust forwarded headers by default --- src/main.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main.ts diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..5c704be --- /dev/null +++ b/src/main.ts @@ -0,0 +1,23 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { NestExpressApplication } from '@nestjs/platform-express'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // Default to not trusting proxy unless explicitly enabled via environment variable + const trustProxyEnv = process.env.TRUST_PROXY; + const shouldTrustProxy = trustProxyEnv === 'true' || trustProxyEnv === '1'; + + if (shouldTrustProxy) { + app.set('trust proxy', true); + } else { + app.set('trust proxy', false); + } + + const port = process.env.PORT || 3000; + await app.listen(port); + console.log(`Application is running on: await app.getUrl()`); +} + +bootstrap(); \ No newline at end of file From 6fdaf90ff9c6dfd576cc7fc13d0bd58cae96aa1a Mon Sep 17 00:00:00 2001 From: AMDYFX Date: Sun, 18 Oct 2026 16:49:05 +0100 Subject: [PATCH 2/4] fix(applications): don't fail the request when notifying fails --- src/applications/applications.controller.ts | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/applications/applications.controller.ts diff --git a/src/applications/applications.controller.ts b/src/applications/applications.controller.ts new file mode 100644 index 0000000..fe53408 --- /dev/null +++ b/src/applications/applications.controller.ts @@ -0,0 +1,50 @@ +import { Controller, Post, Body, Param, HttpCode, HttpStatus, Logger } from '@nestjs/common'; +// Import your application service and notification service + +@Controller('applications') +export class ApplicationsController { + private readonly logger = new Logger(ApplicationsController.name); + + constructor( + private readonly applicationsService: any, + private readonly notificationService: any + ) {} + + @Post() + @HttpCode(HttpStatus.CREATED) + async createApplication(@Body() createDto: any) { + // 1. Commit application creation + const application = await this.applicationsService.create(createDto); + + // 2. Trigger notification with non-blocking error catching + try { + await this.notificationService.notify({ + type: 'application_created', + targetId: application.id, + }); + } catch (error) { + this.logger.error(`Failed to record notification log for application ${application.id}: ${error.message}`, error.stack); + } + + return application; + } + + @Post(':id/review') + @HttpCode(HttpStatus.OK) + async reviewApplication(@Param('id') id: string, @Body() reviewDto: any) { + // 1. Commit application review update + const updatedApplication = await this.applicationsService.review(id, reviewDto); + + // 2. Trigger notification with non-blocking error catching + try { + await this.notificationService.notify({ + type: 'application_reviewed', + targetId: updatedApplication.id, + }); + } catch (error) { + this.logger.error(`Failed to record notification log for application review ${updatedApplication.id}: ${error.message}`, error.stack); + } + + return updatedApplication; + } +} \ No newline at end of file From 41724db6312de6a23200e1b6089e8c0bfc2f2f61 Mon Sep 17 00:00:00 2001 From: AMDYFX Date: Sun, 18 Oct 2026 16:49:44 +0100 Subject: [PATCH 3/4] fix(donors): aggregate and paginate in the database --- src/donors/donors.service.ts | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/donors/donors.service.ts diff --git a/src/donors/donors.service.ts b/src/donors/donors.service.ts new file mode 100644 index 0000000..b778fde --- /dev/null +++ b/src/donors/donors.service.ts @@ -0,0 +1,60 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; // adjust path as needed + +@Injectable() +export class DonorsService { + constructor(private readonly prisma: PrismaService) {} + + async getDonors(limit = 20, cursor?: string) { + let cursorSortValue: number | undefined; + let cursorId: string | undefined; + + if (cursor) { + try { + const decoded = Buffer.from(cursor, 'base64').toString('utf8'); + const [sortVal, id] = decoded.split('_'); + cursorSortValue = parseFloat(sortVal); + cursorId = id; + if (isNaN(cursorSortValue) || !cursorId) { + throw new Error('Invalid cursor format'); + } + } catch (err) { + throw new BadRequestException('Invalid pagination cursor'); + } + } + + // Using Prisma raw query or optimized relation aggregation for keyset pagination + // Example keyset logic: order by totalAmount DESC, id DESC + const donors = await this.prisma.$queryRaw` + SELECT + d.id, + d.name, + COALESCE(SUM(s.amount), 0) as "totalAmount" + FROM "Donor" d + LEFT JOIN "Stream" s ON s."donorId" = d.id + GROUP BY d.id, d.name + ${ + cursorSortValue !== undefined && cursorId !== undefined + ? Prisma.sql`HAVING (COALESCE(SUM(s.amount), 0), d.id) < (${cursorSortValue},${cursorId})` + : Prisma.empty + } + ORDER BY "totalAmount" DESC, d.id DESC + LIMIT ${limit + 1} + `; + + const hasMore = (donors as any[]).length > limit; + const paginatedDonors = hasMore ? (donors as any[]).slice(0, limit) : donors; + + let nextCursor: string | null = null; + if (hasMore && paginatedDonors.length > 0) { + const lastItem = paginatedDonors[paginatedDonors.length - 1]; + const rawCursorString = `${lastItem.totalAmount}_${lastItem.id}`; + nextCursor = Buffer.from(rawCursorString).toString('base64'); + } + + return { + data: paginatedDonors, + nextCursor, + }; + } +} \ No newline at end of file From 3332ebe0fd890033280ce26f10d8c6dfda1cb280 Mon Sep 17 00:00:00 2001 From: AMDYFX Date: Sun, 18 Oct 2026 16:50:29 +0100 Subject: [PATCH 4/4] fix: add a unique tie-breaker to cursor pagination --- src/streams/streams.service.ts | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/streams/streams.service.ts diff --git a/src/streams/streams.service.ts b/src/streams/streams.service.ts new file mode 100644 index 0000000..53c1a43 --- /dev/null +++ b/src/streams/streams.service.ts @@ -0,0 +1,58 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class StreamsService { + constructor(private readonly prisma: PrismaService) {} + + async getStreams(limit = 20, cursor?: string) { + let cursorCreatedAt: Date | undefined; + let cursorId: string | undefined; + + if (cursor) { + try { + const decoded = Buffer.from(cursor, 'base64').toString('utf8'); + const [timeStr, id] = decoded.split('_'); + cursorCreatedAt = new Date(timeStr); + cursorId = id; + if (isNaN(cursorCreatedAt.getTime()) || !cursorId) { + throw new Error('Invalid cursor'); + } + } catch (err) { + throw new BadRequestException('Invalid pagination cursor'); + } + } + + // Keyset pagination query with (createdAt, id) tie-breaker + const streams = await this.prisma.stream.findMany({ + take: limit + 1, + ...(cursorCreatedAt && cursorId + ? { + skip: 1, + cursor: { + id: cursorId, + }, + } + : {}), + orderBy: [ + { createdAt: 'desc' }, + { id: 'desc' }, + ], + }); + + const hasMore = streams.length > limit; + const paginatedStreams = hasMore ? streams.slice(0, limit) : streams; + + let nextCursor: string | null = null; + if (hasMore && paginatedStreams.length > 0) { + const lastItem = paginatedStreams[paginatedStreams.length - 1]; + const rawCursor = `${lastItem.createdAt.toISOString()}_${lastItem.id}`; + nextCursor = Buffer.from(rawCursor).toString('base64'); + } + + return { + data: paginatedStreams, + nextCursor, + }; + } +} \ No newline at end of file