Skip to content

Commit 736e6dd

Browse files
authored
Merge pull request #1730 from danielships/fix/issues-1575-1576-1577-1578-reason-length-webhook-secret-xlm-upload-limits
fix(backend): cap dispute and cancel reasons, require webhook secret length, drop unreachable XLM asset, add upload limits
2 parents 259f17e + 1181df4 commit 736e6dd

5 files changed

Lines changed: 37 additions & 7 deletions

File tree

‎backend/src/documents/documents.controller.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
UseGuards,
1616
} from '@nestjs/common';
1717
import { FileInterceptor } from '@nestjs/platform-express';
18+
import { Throttle } from '@nestjs/throttler';
1819
import {
1920
ApiTags,
2021
ApiOperation,
@@ -44,6 +45,8 @@ export class DocumentsController {
4445
constructor(private readonly documentsService: DocumentsService) {}
4546

4647
@Post('upload')
48+
// Uploads are expensive: far stricter than the global 60 requests/minute.
49+
@Throttle({ default: { limit: 10, ttl: 60_000 } })
4750
@ApiOperation({ summary: 'Upload a document for a shipment' })
4851
@ApiConsumes('multipart/form-data')
4952
@ApiBody({

‎backend/src/documents/documents.service.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Logger,
66
BadRequestException,
77
ConflictException,
8+
PayloadTooLargeException,
89
} from '@nestjs/common';
910
import { InjectRepository } from '@nestjs/typeorm';
1011
import { Repository } from 'typeorm';
@@ -185,6 +186,26 @@ export class DocumentsService {
185186
};
186187
}
187188

189+
/** Total bytes a single user may store across all their uploads. */
190+
private static readonly MAX_USER_STORAGE_BYTES = 500 * 1024 * 1024;
191+
192+
private async assertWithinStorageQuota(
193+
uploaderId: string,
194+
incomingBytes: number,
195+
): Promise<void> {
196+
const rows =
197+
(await this.documentRepo.find({
198+
where: { uploaderId },
199+
select: { sizeBytes: true },
200+
})) ?? [];
201+
const used = rows.reduce((sum, row) => sum + row.sizeBytes, 0);
202+
if (used + incomingBytes > DocumentsService.MAX_USER_STORAGE_BYTES) {
203+
throw new PayloadTooLargeException(
204+
'Upload would exceed your storage quota; delete old documents first',
205+
);
206+
}
207+
}
208+
188209
private async persistDocument(
189210
file: Express.Multer.File | undefined,
190211
uploader: User,
@@ -195,6 +216,7 @@ export class DocumentsService {
195216
if (!file) {
196217
throw new BadRequestException('No file provided');
197218
}
219+
await this.assertWithinStorageQuota(uploader.id, file.size ?? file.buffer?.length ?? 0);
198220
const persisted = this.persistFile(file);
199221

200222
try {

‎backend/src/payments/settlement-asset.util.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
// MVP settlement-asset decision (issue #1276): TOKEN_CONTRACT_ADDRESS must be
1+
// MVP settlement-asset decision (issue #1276): only USD-pegged assets are
2+
// supported (XLM is not a settlement asset: the funding flow always settles
3+
// in USDC). TOKEN_CONTRACT_ADDRESS must be
24
// a USD-pegged SEP-41 asset (e.g. USDC on Stellar), and Shipment.price (fiat
35
// decimal, e.g. 1234.56) converts to the contract's i128 base units via a
46
// fixed decimal shift — no live FX. Stellar SAC-wrapped assets and classic
@@ -12,7 +14,6 @@ const BASE_UNITS_PER_ASSET_UNIT = 10 ** SETTLEMENT_ASSET_DECIMALS;
1214

1315
const SUPPORTED_SETTLEMENT_ASSETS: Record<string, number> = {
1416
USDC: SETTLEMENT_ASSET_DECIMALS,
15-
XLM: SETTLEMENT_ASSET_DECIMALS,
1617
};
1718

1819
export function resolveSettlementAssetDecimals(assetCode: string): number {

‎backend/src/shipments/shipments.controller.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,17 @@ import { RolesGuard } from '../auth/guards/roles.guard';
3030
import { UserRole } from '../common/enums/role.enum';
3131
import { ShipmentStatus } from '../common/enums/shipment-status.enum';
3232
import { User } from '../users/entities/user.entity';
33-
import { IsEnum, IsOptional, IsString } from 'class-validator';
33+
import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
3434
import { ApiPropertyOptional } from '@nestjs/swagger';
3535
import { Response } from 'express';
3636
import { ExportShipmentsDto } from './dto/export-shipments.dto';
3737
import { AnalyticsQueryDto } from './dto/analytics-query.dto';
3838

3939
class DisputeBody {
40-
@ApiPropertyOptional()
40+
@ApiPropertyOptional({ maxLength: 2000 })
4141
@IsOptional()
4242
@IsString()
43+
@MaxLength(2000)
4344
reason?: string;
4445
}
4546

@@ -56,9 +57,10 @@ class ResolveDisputeBody {
5657
}
5758

5859
class CancelBody {
59-
@ApiPropertyOptional()
60+
@ApiPropertyOptional({ maxLength: 2000 })
6061
@IsOptional()
6162
@IsString()
63+
@MaxLength(2000)
6264
reason?: string;
6365
}
6466

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsString, IsUrl, MaxLength } from 'class-validator';
2+
import { IsString, IsUrl, MaxLength, MinLength } from 'class-validator';
33

44
export class CreateWebhookDto {
55
@ApiProperty({ example: 'https://partner.example.com/webhooks/freightflow' })
@@ -8,9 +8,11 @@ export class CreateWebhookDto {
88

99
@ApiProperty({
1010
example: 'whsec_test_123',
11-
description: 'Shared secret used to verify the HMAC signature.',
11+
description:
12+
'Shared secret used to verify the HMAC signature (at least 16 characters).',
1213
})
1314
@IsString()
15+
@MinLength(16)
1416
@MaxLength(255)
1517
secret: string;
1618
}

0 commit comments

Comments
 (0)