Initial commit: teaching institution management API.

Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
2026-08-09 04:18:08 +02:00
commit f04c797be6
107 changed files with 9190 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
// /middlewares/activityLogger.js
'use strict';
const activityLogService = require('../components/activityLogs/activityLogService');
const logger = require('../utils/logger');
const SKIP_PREFIXES = [
'/api/health',
'/api/activity-logs'
];
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
const resolveResource = (path = '') => {
const clean = path.split('?')[0].replace(/^\/api\//, '');
const [resource] = clean.split('/').filter(Boolean);
return resource || 'unknown';
};
const resolveResourceId = (req) => {
if (req.params?.id) return String(req.params.id);
if (req.params?.userId) return String(req.params.userId);
if (req.body?._id) return String(req.body._id);
if (req.body?.id) return String(req.body.id);
return null;
};
const resolveAction = (method, path = '') => {
const p = path.toLowerCase();
if (p.includes('/auth/login')) return 'login';
if (p.includes('/auth/logout')) return 'logout';
if (p.includes('/auth/refresh')) return 'other';
if (p.includes('enroll') || p.includes('register')) return 'enroll';
if (p.includes('attendance')) return 'attendance';
if (p.includes('upload')) return 'upload';
if (p.includes('retry')) return 'retry';
switch (method) {
case 'POST':
return 'create';
case 'PUT':
case 'PATCH':
return 'update';
case 'DELETE':
return 'delete';
default:
return 'other';
}
};
const shouldSkip = (req) => {
if (!MUTATING_METHODS.has(req.method)) return true;
const path = req.originalUrl || req.url || '';
if (SKIP_PREFIXES.some((prefix) => path.startsWith(prefix))) return true;
if (path.includes('/auth/refresh')) return true;
return false;
};
const activityLogger = (req, res, next) => {
if (shouldSkip(req)) {
return next();
}
res.on('finish', () => {
// Only persist completed mutating requests (success or business failure)
if (res.statusCode < 200 || res.statusCode >= 600) return;
const path = (req.originalUrl || req.url || '').split('?')[0];
const action = resolveAction(req.method, path);
const actor = req.user || null;
const payload = {
actor: actor?._id || null,
actorUsername: actor?.username || req.body?.username || null,
actorName: actor
? `${actor.name || ''} ${actor.surname || ''}`.trim() || actor.username
: req.body?.username || null,
action,
resource: resolveResource(path),
resourceId: resolveResourceId(req),
method: req.method,
path,
statusCode: res.statusCode,
ip: req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || null,
userAgent: req.headers['user-agent'] || null,
metadata: {
params: activityLogService.sanitizeValue(req.params || {}),
query: activityLogService.sanitizeValue(req.query || {}),
body: activityLogService.sanitizeValue(req.body || {})
}
};
activityLogService.createActivityLog(payload).catch((err) => {
logger.error(`[ActivityLog] Failed to write log: ${err.message}`);
});
});
next();
};
module.exports = activityLogger;
+43
View File
@@ -0,0 +1,43 @@
// /middlewares/authMiddleware.js
const jwt = require('jsonwebtoken');
const config = require('../config/config');
const AppError = require('../utils/AppError');
const User = require('../components/users/userModel');
const authMiddleware = async (req, res, next) => {
try {
let token = null;
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.split(' ')[1];
}
if (!token) {
return next(new AppError('UNAUTHORIZED'));
}
let decoded;
try {
decoded = jwt.verify(token, config.JWT_ACCESS_SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') {
return next(new AppError('TOKEN_EXPIRED'));
}
return next(new AppError('UNAUTHORIZED'));
}
const user = await User.findById(decoded.id).populate('role');
if (!user || !user.isActive) {
return next(new AppError('UNAUTHORIZED'));
}
req.user = user;
next();
} catch (error) {
next(error);
}
};
module.exports = authMiddleware;
+107
View File
@@ -0,0 +1,107 @@
// /middlewares/globalErrorHandler.js
const logger = require('../utils/logger');
const { errorResponse } = require('../utils/apiResponse');
const { getLocalizedErrorMessage } = require('../utils/errorLocalization');
const formatDetailsMessage = (details) => {
if (!details) return null;
if (typeof details === 'string') return details;
if (Array.isArray(details)) {
const parts = details
.map((item) => {
if (!item) return null;
if (typeof item === 'string') return item;
return item.message || item.reason || null;
})
.filter(Boolean);
return parts.length ? parts.join(' — ') : null;
}
if (typeof details === 'object') {
const parts = Object.entries(details).map(([key, value]) => {
if (value == null) return null;
if (typeof value === 'string') return `${key}: ${value}`;
if (typeof value === 'object' && value.message) return `${key}: ${value.message}`;
if (key === 'keyValue') return Object.entries(value).map(([k, v]) => `${k}=${v}`).join(', ');
return null;
}).filter(Boolean);
return parts.length ? parts.join(' — ') : null;
}
return null;
};
const globalErrorHandler = (err, req, res, next) => {
let statusCode = err.statusCode || 500;
let errorCode = err.errorCode || 'INTERNAL_SERVER_ERROR';
let details = err.details || null;
// Handle Mongoose CastError (e.g. invalid ObjectId)
if (err.name === 'CastError') {
statusCode = 400;
errorCode = 'VALIDATION_FAILED';
details = { field: err.path, value: err.value, reason: 'Invalid ObjectId format' };
}
// Handle Mongoose Duplicate Key Error (E11000)
if (err.code === 11000) {
statusCode = 409;
errorCode = 'DUPLICATE_KEY';
details = { keyPattern: err.keyPattern, keyValue: err.keyValue };
}
// Handle Mongoose ValidationError
if (err.name === 'ValidationError') {
statusCode = 400;
errorCode = 'VALIDATION_FAILED';
details = Object.keys(err.errors || {}).reduce((acc, key) => {
acc[key] = err.errors[key].message;
return acc;
}, {});
}
// Handle Joi validation errors if forwarded as standard Error
if (err.isJoi) {
statusCode = 400;
errorCode = 'VALIDATION_FAILED';
details = err.details
? err.details.map((d) => ({ message: d.message, path: d.path }))
: null;
}
// Localized catalog message (Accept-Language / ?lang=)
let localizedMessage = err.overrideMessage
? err.overrideMessage
: getLocalizedErrorMessage(errorCode, req);
const detailsMessage = formatDetailsMessage(details);
if (detailsMessage && errorCode === 'VALIDATION_FAILED' && !err.overrideMessage) {
localizedMessage = `${localizedMessage} ${detailsMessage}`;
} else if (detailsMessage && errorCode === 'DUPLICATE_KEY') {
localizedMessage = `${localizedMessage} (${detailsMessage})`;
}
// Log error (stack trace for non-operational / 500 errors)
if (!err.isOperational || statusCode >= 500) {
logger.error(`[PROGRAMMING_ERROR] ${err.message}`, {
stack: err.stack,
path: req.originalUrl,
method: req.method
});
} else {
logger.warn(`[OPERATIONAL_ERROR] Code: ${errorCode} | Path: ${req.originalUrl} | Message: ${localizedMessage}`);
}
return errorResponse(
res,
statusCode,
errorCode,
localizedMessage,
details
);
};
module.exports = globalErrorHandler;
+9
View File
@@ -0,0 +1,9 @@
// /middlewares/notFoundHandler.js
const AppError = require('../utils/AppError');
const notFoundHandler = (req, res, next) => {
next(new AppError('NOT_FOUND', { path: req.originalUrl, method: req.method }));
};
module.exports = notFoundHandler;
+35
View File
@@ -0,0 +1,35 @@
// /middlewares/permissionMiddleware.js
const AppError = require('../utils/AppError');
const requirePermission = (requiredPermission) => {
return (req, res, next) => {
try {
if (!req.user) {
return next(new AppError('UNAUTHORIZED'));
}
const role = req.user.role;
if (!role) {
return next(new AppError('FORBIDDEN'));
}
// Check if user is superAdmin (by role name or system status)
if (role.name === 'superAdmin' || (role.isSystem && role.permissions.includes('*'))) {
return next();
}
// Check if required permission is held by role
if (Array.isArray(role.permissions) && role.permissions.includes(requiredPermission)) {
return next();
}
return next(new AppError('FORBIDDEN'));
} catch (error) {
next(error);
}
};
};
module.exports = requirePermission;
module.exports.requires = requirePermission;