diff --git a/src/controllers/activityLogController.js b/src/controllers/activityLogController.js new file mode 100644 index 0000000000..87d017c913 --- /dev/null +++ b/src/controllers/activityLogController.js @@ -0,0 +1,47 @@ +const ActivityLog = require('../models/activityLog'); +const StudentLog = require('../models/studentLog'); +const { hasPermission } = require('../utilities/permissions'); + +const activityLogController = function () { + async function fetchSupportDailyLog(req, res) { + try { + const { studentId } = req.params; + const { requestor } = req.body; + + if (!studentId) return res.status(400).json({ error: 'Missing studentId' }); + + if (!(await hasPermission(requestor, 'fetchSupportDailyLog'))) { + return res + .status(403) + .json({ error: 'Forbidden: Only support role can access this endpoint' }); + } + + // Fetch logs for the specified student asssuming this + // student is a assigned to the support staff. + // Need to add a check for that in the future. + const logs = await StudentLog.find({studentId}).sort({date:-1}) ; + + + // Log the activity of viewing the student's daily log + await ActivityLog.create({ + actor_id: requestor.requestorId, + action_type: 'view_student_daily_log', + metadata: { viewedStudentId: studentId }, + created_at: new Date(), + }); + + // Return the all logs to the client. + res.json(logs); + + } catch (err) { + console.log(err); + res.status(500).json({ error: err.message }); + } + } + + return { + fetchSupportDailyLog, + }; +}; + +module.exports = activityLogController; diff --git a/src/models/activityLog.js b/src/models/activityLog.js new file mode 100644 index 0000000000..0e4751d136 --- /dev/null +++ b/src/models/activityLog.js @@ -0,0 +1,27 @@ +const mongoose = require('mongoose'); + +const ActivityLogSchema = new mongoose.Schema( + { + actor_id: { + type: mongoose.Schema.Types.ObjectId, + ref: 'UserProfile', + required: true, + }, + action_type: { + type: String, + required: true, + }, + metadata: { + type: mongoose.Schema.Types.Mixed, + default: {}, + }, + }, + { + timestamps: { + createdAt: 'created_at', + updatedAt: false, + }, + }, +); + +module.exports = mongoose.model('ActivityLog', ActivityLogSchema); diff --git a/src/models/studentLog.js b/src/models/studentLog.js new file mode 100644 index 0000000000..5916c0d686 --- /dev/null +++ b/src/models/studentLog.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); + +const StudentLogSchema = new mongoose.Schema({ + studentId: { + type: String, + required: true + }, + date: { + type: Date, + required: true + }, + log: { + type: String, + required: true + }, +}) + +module.exports = mongoose.model('StudentLog', StudentLogSchema); + diff --git a/src/routes/activityLogRouter.js b/src/routes/activityLogRouter.js new file mode 100644 index 0000000000..12a2b30c77 --- /dev/null +++ b/src/routes/activityLogRouter.js @@ -0,0 +1,11 @@ +const express = require('express'); +const activityLogRouter = express.Router(); +// Invoke the controller factory +const controller = require('../controllers/activityLogController')(); + +activityLogRouter + .route('/:studentId') + .get(controller.fetchSupportDailyLog); + +// Export the INSTANCE +module.exports = activityLogRouter; diff --git a/src/startup/middleware.js b/src/startup/middleware.js index d6f14cee43..1372f7b401 100644 --- a/src/startup/middleware.js +++ b/src/startup/middleware.js @@ -1,14 +1,14 @@ /* eslint-disable complexity */ /* eslint-disable no-magic-numbers */ -const jwt = require('jsonwebtoken'); -const moment = require('moment'); + const express = require('express'); -const config = require('../config'); const webhookController = require('../controllers/lbdashboard/webhookController'); // your new controller const { Bids } = require('../models/lbdashboard/bids'); // or wherever you're getting Bids const { webhookTest } = webhookController(Bids); +const jwtVerificationLogic = require('../utilities/jwtVerificationLogic'); + const paypalAuthMiddleware = (req, res, next) => { const authHeader = req.header('Paypal-Auth-Algo'); if (!authHeader) { @@ -123,40 +123,31 @@ module.exports = function (app) { if (openPaths.includes(req.path)) { return next(); // Allow PayPal requests through } - if (!req.header('Authorization')) { - res.status(401).send({ 'error:': 'Unauthorized request' }); - return; - } - const authToken = req.header(config.REQUEST_AUTHKEY); - let payload = ''; + // HEADER EXTRACTION + const authHeader = req.header('Authorization'); + const payload = jwtVerificationLogic(authHeader, res); - try { - payload = jwt.verify(authToken, config.JWT_SECRET); - } catch (error) { - res.status(401).send('Invalid token'); - return; - } - if ( - !payload || - !payload.expiryTimestamp || - !payload.userid || - !payload.role || - moment().isAfter(payload.expiryTimestamp) - ) { - res.status(401).send('Unauthorized request'); - return; - } + // FIX: If payload is a response object (meaning logic already sent a 401), STOP HERE. + if (res.headersSent) return; + + // ATTACH DATA & CONTINUE + // Now we know payload is the valid decoded token + const requestor = { + requestorId: payload.userid, + role: payload.role, + permissions: payload.permissions, + }; - const requestor = {}; - requestor.requestorId = payload.userid; - requestor.role = payload.role; - requestor.permissions = payload.permissions; + req.user = requestor; - req.body.requestor = requestor; - next(); + if (req.body) { + req.body.requestor = requestor; + } + + return next(); }); - // Apply PayPal middleware only to specific route + // PROTECTED ROUTES app.post('/api/lb/myWebhooks/', paypalAuthMiddleware, webhookTest); }; diff --git a/src/startup/routes.js b/src/startup/routes.js index 49c325c22b..b91870833c 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -431,11 +431,15 @@ const recipeRouter = require('../routes/kitchenInventory/recipeRouter')(); const jobHitsAndApplicationsRoutes = require('../routes/jobAnalytics/JobHitsAndApplicationsRoutes'); + // Education Portal const educatorRoutes = require('../routes/educatorRoutes'); +const activityLogRouter = require('../routes/activityLogRouter'); + module.exports = function (app) { app.use('/api/bm/summary-dashboard', summaryDashboardRouter); + app.use('/api/support/daily-log', activityLogRouter); app.use('/api', forgotPwdRouter); app.use('/api', loginRouter); app.use('/api', forcePwdRouter); diff --git a/src/utilities/jwtVerificationLogic.js b/src/utilities/jwtVerificationLogic.js new file mode 100644 index 0000000000..b9ed6f0868 --- /dev/null +++ b/src/utilities/jwtVerificationLogic.js @@ -0,0 +1,34 @@ +const moment = require('moment'); +const config = require('../config'); +const jwt = require('jsonwebtoken'); + +const jwtVerificationLogic = (authHeader, res) => { + if (!authHeader) { + return res.status(401).json({ error: 'Unauthorized request: No header' }); + } + + let authToken = authHeader; + // If it has Bearer, strip it. If not, use it as is. + if (authHeader.startsWith('Bearer ')) { + [, authToken] = authHeader.split(' '); + } + + let payload; + try { + payload = jwt.verify(authToken, config.JWT_SECRET); + } catch (error) { + return res.status(401).json({ error: 'Invalid token', details: error.message }); + } + + // Ensure mock date in test is not in the past + const isExpired = moment().isAfter(payload.expiryTimestamp); + if (!payload.userid || !payload.role || isExpired) { + // eslint-disable-next-line no-console + console.log('Auth failed. Expiry:', payload.expiryTimestamp); + return res.status(401).send('Unauthorized request: Token expired or invalid payload'); + } + + return payload; +}; + +module.exports = jwtVerificationLogic; \ No newline at end of file