feat: enhance configuration and rate limiting

- Added new configuration options: trustProxy, uploadConcurrency, batchMaxItems, batchMaxSizeBytes, and maxRequestBodyBytes to AppConfig.
- Implemented utility functions for parsing environment variables and masking sensitive data.
- Updated rate limiting logic to use configurable window size and maximum requests per window.
- Introduced a middleware for rate limiting on specific routes.
- Refactored file handling routes to support streaming downloads instead of redirects.
- Improved error handling and response formatting in file routes.
- Added support for oversized request rejection based on Content-Length header.
- Updated Swagger documentation to reflect changes in API behavior and responses.
- Enhanced tests to cover new features and ensure proper functionality.
This commit is contained in:
MythEclipse
2026-05-29 03:33:39 +07:00
parent be813b1c0e
commit 5425f6d33d
16 changed files with 466 additions and 201 deletions
+37 -18
View File
@@ -13,7 +13,7 @@ beforeAll(async () => {
} catch {
// Fallback 1x1px JPEG
realPhotoBuffer = Buffer.from(
'ffd8ffe000104a46494600010101006000600000ffdb004300080606070605080707070909080a0c140d0c0b0b0c1912130f141d1a1f1e1d1a1c1c20242e2720222c231c1c2837292c30313434341f27393d38323c2e333432ffc0000b080001000101011100ffc4001f0000010501010110000000000000000000000102030405060708ffda000c03010002110311003f00a0ffd9',
'ffd8ffe000104a46494600010101006000600000ffdb004300080606070605080707070909080a0c140d0c0b0b0c1912130f141d1a1f1e1d1a1c1c20242e2720222c231c1c2837292c30313434341f27393d38323c2e333432ffc0b000080100010101011100ffc4001f0000010501010110000000000000000000000102030405060708ffda000c03010002110311003f00a0ffd9',
'hex',
);
}
@@ -22,8 +22,6 @@ beforeAll(async () => {
// Mock db
type UploadResponseBody = {
public_id: string;
telegram_file_id: string;
telegram_file_unique_id: string;
file_name: string;
file_type: string;
download_url: string;
@@ -79,19 +77,17 @@ const mockForwardToStorage = mock(() =>
}),
);
const mockGetFile = mock(() =>
Promise.resolve({
file_id: 'tg-file-id-123',
file_size: 1000,
mime_type: 'image/jpeg',
}),
);
mock.module('../src/utils/telegram', () => ({
forwardToStorage: mockForwardToStorage,
getBot: () => ({
telegram: {
getFile: mockGetFile,
getFile: mock(() =>
Promise.resolve({
file_id: 'tg-file-id-123',
file_size: 1000,
mime_type: 'image/jpeg',
}),
),
},
}),
}));
@@ -106,7 +102,6 @@ describe('Upload Route Handler', () => {
mockWhere.mockClear();
mockLimit.mockClear();
mockForwardToStorage.mockClear();
mockGetFile.mockClear();
mockSelectResult = [];
const uploadRoute = await import('../src/routes/upload');
handleUpload = uploadRoute.handleUpload;
@@ -144,10 +139,15 @@ describe('Upload Route Handler', () => {
const body = await uploadResponseJson(res);
expect(body.public_id).toContain('mocked-nanoid-id');
expect(body.telegram_file_id).toBe('tg-file-id-123');
expect(body.telegram_file_unique_id).toBe('tg-unique-id-abc');
expect(body.file_name).toBe('test.png');
expect(body.file_type).toBe('photo');
expect(body.download_url).toContain('/f/');
// No internal Telegram IDs in public response
expect(body).not.toHaveProperty('telegram_file_id');
expect(body).not.toHaveProperty('telegram_file_unique_id');
expect(body).not.toHaveProperty('storage_chat_id');
expect(body).not.toHaveProperty('storage_message_id');
expect(body).not.toHaveProperty('uploader_id');
});
it('should reject JSON upload without file key', async () => {
@@ -182,6 +182,7 @@ describe('Upload Route Handler', () => {
const body = await uploadResponseJson(res);
expect(body.public_id).toContain('mocked-nanoid-id');
expect(body.file_name).toBe('test_multi.png');
expect(body).not.toHaveProperty('telegram_file_id');
});
it('should deduplicate multipart upload if hash exists', async () => {
@@ -216,10 +217,9 @@ describe('Upload Route Handler', () => {
const body = await uploadResponseJson(res);
expect(body.public_id).toBe('existing-id-123');
expect(body.telegram_file_id).toBe('existing-tg-id');
expect(body.telegram_file_unique_id).toBe('existing-tg-unique');
expect(body.file_name).toBe('existing_name.txt');
expect(body.download_url).toContain('/f/existing-id-123');
expect(body).not.toHaveProperty('telegram_file_id');
// DB query happened
expect(mockSelect).toHaveBeenCalled();
@@ -263,9 +263,9 @@ describe('Upload Route Handler', () => {
const body = await uploadResponseJson(res);
expect(body.public_id).toBe('existing-json-id');
expect(body.telegram_file_id).toBe('existing-tg-json-id');
expect(body.file_name).toBe('existing_json.txt');
expect(body.download_url).toContain('/f/existing-json-id');
expect(body).not.toHaveProperty('telegram_file_id');
// DB query happened
expect(mockSelect).toHaveBeenCalled();
@@ -275,6 +275,25 @@ describe('Upload Route Handler', () => {
expect(mockInsert).not.toHaveBeenCalled();
});
it('should reject oversized request by Content-Length header', async () => {
const req = new Request('http://localhost:3000/api/upload', {
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': String(3 * 1024 * 1024 * 1024),
},
body: JSON.stringify({
file: Buffer.from('hello').toString('base64'),
fileName: 'test.txt',
}),
});
const res = await handleUpload(req);
expect(res.status).toBe(413);
const body = await uploadResponseJson(res);
expect(body.error).toContain('too large');
});
afterAll(() => {
mock.restore();
});