Skip to content
Merged
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
8 changes: 4 additions & 4 deletions backend/src/controllers/assignmentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ export class AssignmentController {

const submissionData = req.body;

if (req.files && Array.isArray(req.files)) {
if ((req as any).files && Array.isArray((req as any).files)) {
const uploadedFiles = await this.fileUploadService.uploadFiles(
req.files,
(req as any).files,
`assignments/${assignmentId}/submissions/${user.id}`
);
submissionData.files = uploadedFiles;
Expand Down Expand Up @@ -324,9 +324,9 @@ export class AssignmentController {
throw new ForbiddenError('Cannot update submission');
}

if (req.files && Array.isArray(req.files)) {
if ((req as any).files && Array.isArray((req as any).files)) {
const uploadedFiles = await this.fileUploadService.uploadFiles(
req.files,
(req as any).files,
`assignments/${submission.assignmentId}/submissions/${user.id}`
);
updateData.files = [...(submission.files || []), ...uploadedFiles];
Expand Down
12 changes: 6 additions & 6 deletions backend/src/controllers/contentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export class ContentController {
throw new AuthError('Unauthorized');
}

if (!req.file) {
if (!(req as any).file) {
throw new ValidationError('No file uploaded');
}

Expand All @@ -216,9 +216,9 @@ export class ContentController {
};

const result = await this.mediaService.uploadMedia(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
(req as any).file.buffer,
(req as any).file.originalname,
(req as any).file.mimetype,
userId,
options
);
Expand Down Expand Up @@ -352,12 +352,12 @@ export class ContentController {
throw new AuthError('Unauthorized');
}

if (!req.file) {
if (!(req as any).file) {
throw new ValidationError('No file uploaded');
}

const format = req.body.format || 'json';
const data = req.file.buffer.toString('utf-8');
const data = (req as any).file.buffer.toString('utf-8');

const result = await this.contentService.importContent(data, format, userId);

Expand Down
2 changes: 1 addition & 1 deletion backend/src/controllers/courseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ router.post(
async (req: Request, res: Response, next: NextFunction) => {
try {
const { query: searchQuery, filters = {}, sessionId } = req.body;
const userId = req.user?.id; // Assuming auth middleware sets req.user
const userId = req.user?.id;

logger.info(`Search request - Query: ${searchQuery}, User: ${userId}`);

Expand Down
12 changes: 6 additions & 6 deletions backend/src/middleware/sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ export const sanitizeInput = (req: Request, res: Response, next: NextFunction) =
if (uploadRequest.params) {
uploadRequest.params = sanitizeRecursive(uploadRequest.params);
}
if (uploadRequest.file) {
sanitizeFileMetadata(uploadRequest.file);
if ((req as any).file) {
sanitizeFileMetadata((req as any).file);
}
if (uploadRequest.files) {
if (Array.isArray(uploadRequest.files)) {
uploadRequest.files.forEach(sanitizeFileMetadata);
if ((req as any).files) {
if (Array.isArray((req as any).files)) {
(req as any).files.forEach(sanitizeFileMetadata);
} else {
Object.values(uploadRequest.files).forEach((fileArray: any) => {
Object.values((req as any).files).forEach((fileArray: any) => {
fileArray.forEach(sanitizeFileMetadata);
});
}
Expand Down
4 changes: 1 addition & 3 deletions backend/src/middleware/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,7 @@ export const checkBlacklist = async (req: Request, res: Response, next: NextFunc
const blockReason = await (securityService as any).isIPBlocked(ip);
if (blockReason) {
logger.warn(`Blocked request from blacklisted IP: ${ip} Reason: ${blockReason}`);
const err = new ForbiddenError('Access denied from this IP.');
err.details = { reason: blockReason };
return next(err);
return next(new ForbiddenError('Access denied from this IP.', { reason: blockReason }));
}

const duration = process.hrtime(start);
Expand Down
4 changes: 3 additions & 1 deletion backend/src/middleware/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Request, RequestHandler } from 'express';
const storage = multer.memoryStorage();

// File filter for security
// @ts-ignore - multer type compatibility
const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
// Allowed file types
const allowedTypes = [
Expand Down Expand Up @@ -107,7 +108,8 @@ export const uploadWithValidation = (options: {
}) => {
return multer({
storage: multer.memoryStorage(),
fileFilter: (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
// @ts-ignore - multer type compatibility
fileFilter: (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
const allowedTypes = options.allowedTypes || [
'application/pdf',
'application/msword',
Expand Down
217 changes: 217 additions & 0 deletions backend/src/services/cacheMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Cache Middleware
* Implements cache-aside pattern for Express endpoints.
* Integrates with MultiTierCache and CacheAnalytics services.
*/

const MultiTierCache = require('./multiTierCache');
const CacheAnalytics = require('./cacheAnalytics');
const logger = require('../utils/logger');

// ────────────────────────────────────────────────────────────────────────────
// Singleton cache instance
// ────────────────────────────────────────────────────────────────────────────

let cacheInstance = null;
let analyticsInstance = null;

const getCache = () => {
if (!cacheInstance) {
cacheInstance = new MultiTierCache({
l1: {
maxSize: 1000,
ttl: 300000, // 5 min default
cleanupInterval: 60000, // 1 min
},
l2: {
ttl: 3600000, // 1 hour default for Redis
keyPrefix: 'cache:',
},
});

cacheInstance.on('error', ({ operation, key, error }) => {
logger.warn(`Cache error [${operation}] key=${key}: ${error.message}`);
});

// Initialize analytics
analyticsInstance = new CacheAnalytics({
logFile: './cache-analytics.log',
});
analyticsInstance.start();
}
return cacheInstance;
};

const getAnalytics = () => {
if (!analyticsInstance) {
getCache(); // init both
}
return analyticsInstance;
};

// ────────────────────────────────────────────────────────────────────────────
// Cache Middleware (Cache-Aside Pattern)
// ────────────────────────────────────────────────────────────────────────────

/**
* Creates an Express middleware that caches GET responses.
*
* @param {Object} options
* @param {number} options.ttl - Time-to-live in seconds
* @param {string} options.keyPrefix - Key prefix for cache isolation
* @param {string[]} [options.tags] - Tags for tag-based invalidation
* @returns {Function} Express middleware
*/
const cacheMiddleware = (options = {}) => {
const { ttl = 60, keyPrefix = 'cache:', tags = [] } = options;
const ttlMs = ttl * 1000;

return async (req, res, next) => {
if (req.method !== 'GET') return next();

// Honor Cache-Control: no-cache header
const cacheControl = req.headers['cache-control'];
if (cacheControl && cacheControl.includes('no-cache')) {
res.set('X-Cache', 'BYPASS');
return next();
}

const cache = getCache();

const cacheKey = buildCacheKey(keyPrefix, req);

try {
const cachedValue = await cache.get(cacheKey);
if (cachedValue !== null && cachedValue !== undefined) {
res.set('X-Cache', 'HIT');
res.set('X-Cache-Tier', 'L1');
return res.json(cachedValue);
}
} catch (error) {
// Graceful degradation: log and continue without cache
logger.warn(`Cache retrieval failed for ${cacheKey}: ${error.message}`);
}

res.set('X-Cache', 'MISS');

// Intercept res.json() to capture and cache the response body
const originalJson = res.json.bind(res);
res.json = function (body) {
res.json = originalJson; // Restore original

// Fire-and-forget: cache the response asynchronously
cache.set(cacheKey, body, { ttl: ttlMs, tags }).catch((err) => {
logger.warn(`Failed to cache response for ${cacheKey}: ${err.message}`);
});

return originalJson(body);
};

next();
};
};

// ────────────────────────────────────────────────────────────────────────────
// Cache Invalidation Middleware
// ────────────────────────────────────────────────────────────────────────────

/**
* Creates an Express middleware that invalidates cached entries by tag
* after a successful write operation.
*
* @param {Object} options
* @param {string[]} options.tags - Tags to invalidate
* @returns {Function} Express middleware
*/
const cacheInvalidationMiddleware = (options = {}) => {
const { tags = [] } = options;

return (req, res, next) => {
res.on('finish', () => {
if (res.statusCode >= 200 && res.statusCode < 400 && tags.length > 0) {
const cache = getCache();
Promise.all(tags.map((tag) => cache.invalidateByTag(tag).catch(() => {})))
.catch(() => {});
}
});

next();
};
};

// ────────────────────────────────────────────────────────────────────────────
// Cache Metrics
// ────────────────────────────────────────────────────────────────────────────

const getCacheMetrics = () => {
const cache = getCache();
const metrics = cache.getMetrics();
return {
l1Hits: metrics.l1.hits,
l1Misses: metrics.l1.misses,
l1HitRate: metrics.l1HitRate || 0,
l1Size: metrics.l1Size || 0,
l1MaxSize: metrics.l1MaxSize || 0,
l2Hits: metrics.l2.hits,
l2Misses: metrics.l2.misses,
l2HitRate: metrics.l2HitRate || 0,
totalRequests: metrics.totalRequests || 0,
overallHitRate: metrics.overallHitRate || 0,
averageResponseTime: metrics.averageResponseTime || 0,
};
};

// ────────────────────────────────────────────────────────────────────────────
// Cache Management Utilities
// ────────────────────────────────────────────────────────────────────────────

const flushL1Cache = () => {
const cache = getCache();
cache.l1Cache.clear();
cache.l1AccessTimes.clear();
cache.l1Tags.clear();
};

const flushAllCaches = async () => {
const cache = getCache();
cache.l1Cache.clear();
cache.l1AccessTimes.clear();
cache.l1Tags.clear();
try {
const keys = await cache.redisCluster.keys(`${cache.config.l2.keyPrefix}*`);
if (keys.length > 0) {
await cache.redisCluster.del(...keys);
}
} catch (err) {
logger.warn(`Failed to flush Redis cache: ${err.message}`);
}
};

// ────────────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────────────

const buildCacheKey = (prefix, req) => {
const queryParams = req.query || {};
const sortedQuery = Object.keys(queryParams)
.sort()
.map((key) => `${key}=${queryParams[key]}`)
.join('&');

const path = req.path || req.originalUrl.split('?')[0];
return sortedQuery ? `${prefix}${path}?${sortedQuery}` : `${prefix}${path}`;
};

// ────────────────────────────────────────────────────────────────────────────
// Exports
// ────────────────────────────────────────────────────────────────────────────

module.exports = {
cacheMiddleware,
cacheInvalidationMiddleware,
getCacheMetrics,
flushL1Cache,
flushAllCaches,
getCache,
getAnalytics,
};
Loading
Loading