Skip to content

Commit fb89c88

Browse files
Merge pull request #2504 from bluewave-labs/feature/file-manager-backend
Feature/file manager backend
2 parents 37efd79 + 0b74571 commit fb89c88

File tree

9 files changed

+600
-113
lines changed

9 files changed

+600
-113
lines changed

Servers/controllers/fileManager.ctrl.ts

Lines changed: 344 additions & 81 deletions
Large diffs are not rendered by default.

Servers/domain.layer/models/fileManager/fileManager.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface FileManagerMetadata {
5555
uploaded_by: number;
5656
uploader_name?: string;
5757
uploader_surname?: string;
58+
existsOnDisk?: boolean;
5859
}
5960

6061
@Table({

Servers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ const swaggerDoc = YAML.load("./swagger.yaml");
5353

5454
const app = express();
5555

56+
// Trust proxy to correctly interpret X-Forwarded-For headers for rate limiting
57+
app.set('trust proxy', 1);
58+
5659
const DEFAULT_PORT = "3000";
5760
const DEFAULT_HOST = "localhost";
5861

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @fileoverview Rate Limiting Middleware
3+
*
4+
* Provides production-ready rate limiting for API endpoints to prevent abuse and DoS attacks.
5+
* Uses express-rate-limit with IPv6-safe IP normalization.
6+
*
7+
* Rate Limiters:
8+
* - fileOperationsLimiter: 15 requests/15min (for file uploads, downloads, deletions)
9+
* - generalApiLimiter: 100 requests/15min (for standard API endpoints)
10+
* - authLimiter: 5 requests/15min (for authentication to prevent brute force)
11+
*
12+
* @module middleware/rateLimit
13+
*/
14+
15+
import rateLimit, { Options } from 'express-rate-limit';
16+
import { STATUS_CODE } from '../utils/statusCode.utils';
17+
import { Request, Response } from 'express';
18+
import logger from '../utils/logger/fileLogger';
19+
20+
/**
21+
* Rate limit configuration with time window and request limits
22+
*/
23+
interface RateLimitConfig {
24+
windowMinutes: number;
25+
maxRequests: number;
26+
message: string;
27+
}
28+
29+
/**
30+
* Predefined rate limit configurations for different endpoint types
31+
*/
32+
const RATE_LIMIT_CONFIGS: Record<string, RateLimitConfig> = {
33+
fileOperations: {
34+
windowMinutes: 15,
35+
maxRequests: 15,
36+
message: 'Too many file operation requests from this IP, please try again after 15 minutes',
37+
},
38+
generalApi: {
39+
windowMinutes: 15,
40+
maxRequests: 100,
41+
message: 'Too many requests from this IP, please try again after 15 minutes',
42+
},
43+
auth: {
44+
windowMinutes: 15,
45+
maxRequests: 5,
46+
message: 'Too many authentication attempts from this IP, please try again after 15 minutes',
47+
},
48+
};
49+
50+
/**
51+
* Creates a standardized rate limit error handler
52+
* Returns consistent error format using STATUS_CODE utility
53+
*/
54+
const createRateLimitHandler = (message: string) => {
55+
return (req: Request, res: Response) => {
56+
const clientIp = req.ip || req.socket?.remoteAddress || 'unknown';
57+
logger.warn(`Rate limit exceeded for IP ${clientIp} on ${req.path}: ${message}`);
58+
res.status(429).json(STATUS_CODE[429](message));
59+
};
60+
};
61+
62+
/**
63+
* Creates a rate limiter with the specified configuration
64+
* Uses express-rate-limit's built-in IP extraction and IPv6 normalization
65+
*/
66+
const createRateLimiter = (config: RateLimitConfig) => {
67+
const options: Partial<Options> = {
68+
windowMs: config.windowMinutes * 60 * 1000,
69+
max: config.maxRequests,
70+
standardHeaders: true, // Send rate limit info in RateLimit-* headers
71+
legacyHeaders: false, // Disable X-RateLimit-* headers
72+
handler: createRateLimitHandler(config.message),
73+
// Let express-rate-limit handle IP extraction with IPv6 support
74+
// This automatically uses req.ip with proper IPv6 normalization
75+
};
76+
77+
return rateLimit(options);
78+
};
79+
80+
/**
81+
* Rate limiter for file operations (upload, download, delete)
82+
* Restrictive limits due to expensive I/O operations
83+
*/
84+
export const fileOperationsLimiter = createRateLimiter(RATE_LIMIT_CONFIGS.fileOperations);
85+
86+
/**
87+
* General API rate limiter for standard CRUD endpoints
88+
* Moderate limits for typical operations
89+
*/
90+
export const generalApiLimiter = createRateLimiter(RATE_LIMIT_CONFIGS.generalApi);
91+
92+
/**
93+
* Strict rate limiter for authentication endpoints
94+
* Very restrictive to prevent brute force attacks
95+
*/
96+
export const authLimiter = createRateLimiter(RATE_LIMIT_CONFIGS.auth);

Servers/package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Servers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"crypto": "^1.0.1",
3030
"dotenv": "^16.4.5",
3131
"express": "^4.21.2",
32+
"express-rate-limit": "^8.1.0",
3233
"express-validator": "^7.2.1",
3334
"helmet": "^8.0.0",
3435
"html-to-docx": "^1.8.0",

Servers/routes/fileManager.route.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,27 @@
77
* - POST /file-manager - Upload file (Admin, Reviewer, Editor only)
88
* - GET /file-manager - List all files (All authenticated users)
99
* - GET /file-manager/:id - Download file (All authenticated users)
10+
* - DELETE /file-manager/:id - Delete file (Admin, Reviewer, Editor only)
1011
*
1112
* Access Control:
1213
* - All routes require JWT authentication
13-
* - Upload restricted to Admin, Reviewer, Editor (enforced by authorize middleware)
14+
* - Upload and Delete restricted to Admin, Reviewer, Editor (enforced by authorize middleware)
1415
* - List and Download available to all authenticated users
1516
*
1617
* @module routes/fileManager
1718
*/
1819

1920
import express, { Request, Response, NextFunction } from "express";
20-
import { uploadFile, listFiles, downloadFile } from "../controllers/fileManager.ctrl";
21+
import { uploadFile, listFiles, downloadFile, removeFile } from "../controllers/fileManager.ctrl";
2122
import authenticateJWT from "../middleware/auth.middleware";
2223
import authorize from "../middleware/accessControl.middleware";
24+
import { fileOperationsLimiter } from "../middleware/rateLimit.middleware";
2325
import multer from "multer";
2426
import { STATUS_CODE } from "../utils/statusCode.utils";
2527
import * as path from "path";
2628
import * as fs from "fs";
2729
import { ALLOWED_MIME_TYPES } from "../utils/validations/fileManagerValidation.utils";
30+
import logger from "../utils/logger/fileLogger";
2831

2932
const router = express.Router();
3033

@@ -81,12 +84,34 @@ const upload = multer({
8184
* Catches file size limit errors and file type rejection errors
8285
*/
8386
const handleMulterError = (err: any, req: Request, res: Response, next: NextFunction) => {
84-
// Clean up temporary file if it exists
87+
// Clean up temporary file if it exists (async, non-blocking)
8588
if (req.file?.path) {
89+
// Secure containment validation using realpathSync to resolve symlinks
90+
let resolvedPath: string;
91+
let resolvedTempDir: string;
92+
8693
try {
87-
fs.unlinkSync(req.file.path);
88-
} catch (cleanupError) {
89-
console.error("Failed to clean up temporary file:", cleanupError);
94+
// Resolve real paths (follows symlinks) to prevent directory traversal via symlinks
95+
resolvedTempDir = fs.realpathSync(tempDir);
96+
resolvedPath = fs.realpathSync(req.file.path);
97+
98+
// Only clean up if the file is strictly within the temp directory
99+
if (resolvedPath.startsWith(resolvedTempDir + path.sep)) {
100+
// Fire-and-forget async cleanup to avoid blocking
101+
fs.promises.unlink(resolvedPath).catch((cleanupError) => {
102+
// Ignore ENOENT (file already deleted), but log other errors
103+
if (cleanupError.code !== 'ENOENT') {
104+
logger.error("Failed to clean up temporary file:", cleanupError);
105+
}
106+
});
107+
} else {
108+
// Log security violation attempt
109+
logger.warn(`Security: Blocked cleanup attempt outside temp directory. Path: ${resolvedPath}, Allowed: ${resolvedTempDir}`);
110+
}
111+
} catch (e) {
112+
// Unable to resolve file/directory (file may not exist), skip cleanup and continue
113+
logger.warn(`Failed to resolve path for cleanup: ${req.file.path}`, e);
114+
// Do not return - continue to error handling below
90115
}
91116
}
92117

@@ -121,10 +146,12 @@ const handleMulterError = (err: any, req: Request, res: Response, next: NextFunc
121146
* @returns {403} Access denied (unauthorized role)
122147
* @returns {413} File size exceeds maximum allowed size
123148
* @returns {415} Unsupported file type
149+
* @returns {429} Too many requests - rate limit exceeded
124150
* @returns {500} Server error
125151
*/
126152
router.post(
127153
"/",
154+
fileOperationsLimiter,
128155
authenticateJWT,
129156
authorize(["Admin", "Reviewer", "Editor"]),
130157
upload.single("file"),
@@ -139,9 +166,10 @@ router.post(
139166
* @query page - Page number (optional)
140167
* @query pageSize - Items per page (optional)
141168
* @returns {200} List of files with metadata and pagination
169+
* @returns {429} Too many requests - rate limit exceeded
142170
* @returns {500} Server error
143171
*/
144-
router.get("/", authenticateJWT, listFiles);
172+
router.get("/", fileOperationsLimiter, authenticateJWT, listFiles);
145173

146174
/**
147175
* @route GET /file-manager/:id
@@ -151,8 +179,28 @@ router.get("/", authenticateJWT, listFiles);
151179
* @returns {200} File content with download headers
152180
* @returns {403} Access denied (file from different organization)
153181
* @returns {404} File not found
182+
* @returns {429} Too many requests - rate limit exceeded
154183
* @returns {500} Server error
155184
*/
156-
router.get("/:id", authenticateJWT, downloadFile);
185+
router.get("/:id", fileOperationsLimiter, authenticateJWT, downloadFile);
186+
187+
/**
188+
* @route DELETE /file-manager/:id
189+
* @desc Delete a file by ID
190+
* @access Admin, Reviewer, Editor only
191+
* @param id - File ID
192+
* @returns {200} File deleted successfully
193+
* @returns {403} Access denied (file from different organization or unauthorized role)
194+
* @returns {404} File not found
195+
* @returns {429} Too many requests - rate limit exceeded
196+
* @returns {500} Server error
197+
*/
198+
router.delete(
199+
"/:id",
200+
fileOperationsLimiter,
201+
authenticateJWT,
202+
authorize(["Admin", "Reviewer", "Editor"]),
203+
removeFile
204+
);
157205

158206
export default router;

0 commit comments

Comments
 (0)