Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 77 additions & 8 deletions api/src/handlers/files/upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { APIGatewayResponse } from '../../types/api';
import { AuthenticatedEvent, withAuth } from '../../middleware/auth-middleware';
import { FileRepository } from '../../repositories/file-repository.impl';
import { createDatabaseConnection } from '../../utils/database-connection';
import { createLogger } from '../../middleware/logging';
import {
parseMultipartFormData as parseMultipart,
validateMultipartRequest,
} from '../../utils/multipart-parser';
import {
ValidationError,
FileProcessingError,
Expand Down Expand Up @@ -75,10 +80,57 @@ interface FileUploadData {

/**
* Parse multipart form data from API Gateway event
* Simplified parser for base64-encoded binary data
* Supports both multipart/form-data and legacy JSON/base64 formats
*/
function parseMultipartFormData(event: AuthenticatedEvent): FileUploadData {
// For base64-encoded body (API Gateway binary support)
const contentType = event.headers['content-type'] || event.headers['Content-Type'] || '';

// Handle multipart/form-data
if (contentType.includes('multipart/form-data')) {
const validation = validateMultipartRequest(contentType);
if (!validation.isValid || !validation.boundary) {
throw new ValidationError(validation.error || 'Invalid multipart request');
}

if (!event.body) {
throw new ValidationError('No request body provided');
}

// Parse multipart data
const body = event.isBase64Encoded
? Buffer.from(event.body, 'base64')
: Buffer.from(event.body);
const parsed = parseMultipart(body, validation.boundary);

// Get the first file
if (parsed.files.length === 0) {
throw new ValidationError('No file found in multipart request');
}

const file = parsed.files[0];

// Extract metadata from fields
const metadata: Record<string, unknown> = {};
for (const [key, value] of Object.entries(parsed.fields)) {
if (key !== 'file' && key !== 'fileName' && key !== 'mimeType') {
try {
metadata[key] = JSON.parse(value);
} catch {
metadata[key] = value;
}
}
}
Comment on lines +113 to +122

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing arbitrary JSON from multipart fields without validation could allow injection of malicious data structures. Consider validating the parsed metadata against a schema or whitelist of allowed fields before accepting it.

Copilot uses AI. Check for mistakes.

return {
fileName: file.filename,
fileSize: file.size,
mimeType: file.contentType,
fileContent: file.data,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
};
}

// Legacy: For base64-encoded body (API Gateway binary support)
if (event.isBase64Encoded && event.body) {
const buffer = Buffer.from(event.body, 'base64');

Expand Down Expand Up @@ -110,7 +162,7 @@ function parseMultipartFormData(event: AuthenticatedEvent): FileUploadData {
};
}

// For JSON body with base64 file content
// Legacy: For JSON body with base64 file content
if (event.body) {
try {
const body = JSON.parse(event.body);
Expand Down Expand Up @@ -213,7 +265,11 @@ async function uploadToS3(
s3Bucket: S3_BUCKET_NAME,
};
} catch (error) {
console.error('S3 upload error:', error);
const logger = createLogger({ userId, s3Key });
logger.error('S3 upload failed', {
error: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined,
});
throw new ExternalServiceError('S3', 'Failed to upload file to storage', {
error: error instanceof Error ? error.message : 'Unknown error',
});
Expand Down Expand Up @@ -246,7 +302,12 @@ async function storeFileMetadata(

return fileRecord.id;
} catch (error) {
console.error('Database error:', error);
const logger = createLogger({ userId, s3Key });
logger.error('Database operation failed', {
operation: 'storeFileMetadata',
error: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined,
});
throw new DatabaseError('Failed to store file metadata', {
error: error instanceof Error ? error.message : 'Unknown error',
});
Expand Down Expand Up @@ -284,8 +345,14 @@ function createSuccessResponse(
/**
* Create error response
*/
function createErrorResponse(error: Error): APIGatewayResponse {
console.error('Handler error:', error);
function createErrorResponse(error: Error, requestId?: string): APIGatewayResponse {
const logger = createLogger({ requestId });

logger.error('File upload handler error', {
errorType: error.name,
message: error.message,
stack: error.stack,
});

// Handle known error types
if (
Expand Down Expand Up @@ -321,6 +388,8 @@ function createErrorResponse(error: Error): APIGatewayResponse {
* Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 3.6
*/
async function handleFileUpload(event: AuthenticatedEvent): Promise<APIGatewayResponse> {
const requestId = event.requestContext?.requestId;

try {
// Ensure user is authenticated (middleware handles this)
if (!event.user?.userId) {
Expand All @@ -344,7 +413,7 @@ async function handleFileUpload(event: AuthenticatedEvent): Promise<APIGatewayRe
// Return success response
return createSuccessResponse(fileId, fileData.fileName, new Date());
} catch (error) {
return createErrorResponse(error as Error);
return createErrorResponse(error as Error, requestId);
}
}

Expand Down
194 changes: 194 additions & 0 deletions api/src/handlers/health/health-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* Health Check Lambda Handler
* Provides system health status for monitoring
* Requirements: Monitoring and observability
* Following SOLID principles: Single Responsibility
*/

import { APIGatewayEvent, APIGatewayResponse } from '../../types/api';
import { createDatabaseConnection } from '../../utils/database-connection';
import { createLogger } from '../../middleware/logging';

/**
* Health status type
*/
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';

/**
* Component health info
*/
interface ComponentHealth {
status: HealthStatus;
message?: string;
responseTime?: number;
}

/**
* Health check response
*/
interface HealthCheckResponse {
status: HealthStatus;
timestamp: string;
uptime: number;
version: string;
components: {
database: ComponentHealth;
memory: ComponentHealth;
};
}

/**
* Check database health
*/
async function checkDatabaseHealth(): Promise<ComponentHealth> {
const startTime = Date.now();

try {
const db = await createDatabaseConnection();

// Simple query to check connectivity
await db.query('SELECT 1');
await db.disconnect();
Comment on lines +43 to +51

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new database connection for every health check is inefficient and could exhaust connection pools under high load. Consider reusing a connection or implementing a simple ping mechanism that uses an existing connection from the pool.

Suggested change
async function checkDatabaseHealth(): Promise<ComponentHealth> {
const startTime = Date.now();
try {
const db = await createDatabaseConnection();
// Simple query to check connectivity
await db.query('SELECT 1');
await db.disconnect();
async function checkDatabaseHealth(db: { query: (sql: string) => Promise<any> }): Promise<ComponentHealth> {
const startTime = Date.now();
try {
// Simple query to check connectivity
await db.query('SELECT 1');

Copilot uses AI. Check for mistakes.

const responseTime = Date.now() - startTime;

return {
status: 'healthy',
message: 'Database connection successful',
responseTime,
};
} catch (error) {
const responseTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';

return {
status: 'unhealthy',
message: `Database connection failed: ${errorMessage}`,
responseTime,
};
}
}

/**
* Check memory health
*/
function checkMemoryHealth(): ComponentHealth {
const usage = process.memoryUsage();
const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);
const heapTotalMB = Math.round(usage.heapTotal / 1024 / 1024);
const percentUsed = Math.round((usage.heapUsed / usage.heapTotal) * 100);

// Consider unhealthy if using more than 90% of heap
if (percentUsed > 90) {
return {
status: 'unhealthy',
message: `High memory usage: ${heapUsedMB}MB / ${heapTotalMB}MB (${percentUsed}%)`,
};
}

// Consider degraded if using more than 75% of heap
if (percentUsed > 75) {
return {
status: 'degraded',
message: `Elevated memory usage: ${heapUsedMB}MB / ${heapTotalMB}MB (${percentUsed}%)`,
};
}

return {
status: 'healthy',
message: `Memory usage normal: ${heapUsedMB}MB / ${heapTotalMB}MB (${percentUsed}%)`,
};
}

/**
* Determine overall health status
*/
function determineOverallStatus(components: {
database: ComponentHealth;
memory: ComponentHealth;
}): HealthStatus {
// If any component is unhealthy, overall is unhealthy
if (components.database.status === 'unhealthy' || components.memory.status === 'unhealthy') {
return 'unhealthy';
}

// If any component is degraded, overall is degraded
if (components.database.status === 'degraded' || components.memory.status === 'degraded') {
return 'degraded';
}

// All components healthy
return 'healthy';
}

/**
* Main health check handler
*/
async function handleHealthCheck(event: APIGatewayEvent): Promise<APIGatewayResponse> {
const logger = createLogger({
handler: 'healthCheck',
requestId: event.requestContext?.requestId,
});

logger.info('Health check requested');

try {
// Check all components
const [databaseHealth, memoryHealth] = await Promise.all([
checkDatabaseHealth(),
checkMemoryHealth(),
]);

const components = {
database: databaseHealth,
memory: memoryHealth,
};

const overallStatus = determineOverallStatus(components);

const response: HealthCheckResponse = {
status: overallStatus,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.APP_VERSION || '1.0.0',
components,
Comment on lines +149 to +154

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing version information in health check responses could aid attackers in identifying known vulnerabilities. Consider making version exposure configurable or removing it from public health endpoints.

Suggested change
const response: HealthCheckResponse = {
status: overallStatus,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.APP_VERSION || '1.0.0',
components,
// Conditionally include version information based on environment variable
const exposeVersion = process.env.EXPOSE_VERSION_IN_HEALTHCHECK === 'true';
const response: Omit<HealthCheckResponse, 'version'> & Partial<Pick<HealthCheckResponse, 'version'>> = {
status: overallStatus,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
components,
...(exposeVersion && { version: process.env.APP_VERSION || '1.0.0' }),

Copilot uses AI. Check for mistakes.
};

// Return appropriate HTTP status code
const statusCode = overallStatus === 'healthy' ? 200 : overallStatus === 'degraded' ? 200 : 503;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This nested ternary operator for determining the statusCode can be simplified for better readability. A single ternary check against 'unhealthy' is sufficient and more direct.

Suggested change
const statusCode = overallStatus === 'healthy' ? 200 : overallStatus === 'degraded' ? 200 : 503;
const statusCode = overallStatus === 'unhealthy' ? 503 : 200;


logger.info('Health check completed', { status: overallStatus, statusCode });

return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
body: JSON.stringify(response),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Health check failed', { error: errorMessage });

return {
statusCode: 503,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
status: 'unhealthy',
timestamp: new Date().toISOString(),
message: 'Health check failed',
error: errorMessage,
}),
};
}
}

/**
* Lambda handler export
*/
export const handler = handleHealthCheck;
Loading
Loading