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
1,987 changes: 1,986 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test:ci": "npm test -- --coverage",
"lint": "eslint ./src --ext .js,.jsx --max-warnings=2000 --ignore-pattern '**/node_modules/**' --ignore-pattern '**/coverage/**' --ignore-pattern '**/dist/**' --ignore-pattern '**/build/**'",
"lint:fix": "eslint ./src --ext .js,.jsx --fix --ignore-pattern '**/node_modules/**' --ignore-pattern '**/coverage/**' --ignore-pattern '**/dist/**' --ignore-pattern '**/build/**'",
"build": "babel src -d dist && mkdir -p dist/data && cp -r src/data/* dist/data/ 2>/dev/null || true",
"build": "babel src -d dist && shx mkdir -p dist/data && shx cp -r src/data/* dist/data/",
"buildw": "babel src -d dist --watch",
"start": "node dist/server.js",
"dev": "nodemon --exec \"babel-node --only src src/server.js\"",
Expand Down Expand Up @@ -60,6 +60,7 @@
"pidtree": "^0.6.0",
"prettier": "3.2.5",
"puppeteer": "^24.40.0",
"shx": "^0.4.0",
"supertest": "^6.3.4"
},
"dependencies": {
Expand Down Expand Up @@ -87,7 +88,7 @@
"body-parser": "^1.20.4",
"card-validator": "^10.0.2",
"cheerio": "^0.22.0",
"cloudinary": "^2.8.0",
"cloudinary": "^2.9.0",
"compression": "^1.8.0",
"cors": "^2.8.4",
"cron": "^1.8.2",
Expand Down
21 changes: 9 additions & 12 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,36 @@
const express = require('express');
const Sentry = require('@sentry/node');
const testRoutes = require('./routes/testRoutes');

Check warning on line 3 in src/app.js

View workflow job for this annotation

GitHub Actions / Lint Check

There should be no empty line between import groups

const app = express();
const logger = require('./startup/logger');
const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler');

Check warning on line 7 in src/app.js

View workflow job for this annotation

GitHub Actions / Lint Check

There should be no empty line between import groups
// const experienceRoutes = require('./routes/applicantAnalyticsRoutes');

// 1. Core initialization
logger.init();

app.use(Sentry.Handlers.requestHandler());

// Then load all other setup
// 2. Load essential middleware (The "Engine")
require('./startup/compression')(app);
require('./startup/cors')(app);
require('./startup/bodyParser')(app);
require('./startup/session')(app); // Add session before middleware and routes
require('./startup/bodyParser')(app); // <--- Crucial this runs before routes
require('./startup/session')(app);

// 3. Define Routes (The "Destination")
// It is better to move these INSIDE startup/routes.js, but if they stay here:
app.use('/api/test', testRoutes);

const helpFeedbackRouter = require('./routes/helpFeedbackRouter');
const helpRequestRouter = require('./routes/helpRequestRouter');

Check warning on line 25 in src/app.js

View workflow job for this annotation

GitHub Actions / Lint Check

Expected 1 empty line after require statement not followed by another require

app.use('/api/feedback', helpFeedbackRouter);
app.use('/api/helprequest', helpRequestRouter);

require('./startup/middleware')(app);
// This handles all other routes and likely has your 404 handler
require('./startup/routes')(app);

const weeklyReportsRouter = require('./routes/weeklyReportsRouter');

app.use('/api', weeklyReportsRouter);

// ⚠ This must come *after* your custom /api routes
require('./startup/routes')(app);

// 4. Error Handling (The "Safety Net")
app.use(Sentry.Handlers.errorHandler());
app.use(globalErrorHandler);

Expand Down
53 changes: 53 additions & 0 deletions src/controllers/activityLogController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const ActivityLog = require('../models/activityLog');
const UserProfile = require('../models/userProfile');
const { hasPermission } = require('../utilities/permissions');

const activityLogController = function () {
async function fetchSupportDailyLog(req, res) {

Check warning on line 6 in src/controllers/activityLogController.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move async function 'fetchSupportDailyLog' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ2t2LnEuW9FAmBC6s5u&open=AZ2t2LnEuW9FAmBC6s5u&pullRequest=1898
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' });
}

const studentProfile = await UserProfile.findById(studentId).select('orgId');
if (!studentProfile) {
return res.status(404).json({ error: 'Student not found' });
}

if (String(studentProfile.orgId) !== String(requestor.orgId)) {
return res
.status(403)
.json({ error: 'Forbidden: Cannot access student outside your organization' });
}

// fetch logs
const logs = await ActivityLog.find({ actor_id: studentId })
.sort({ created_at: -1 })
.select('action_type metadata created_at actor_id');

await ActivityLog.create({
actor_id: requestor.requestorId,
action_type: 'view_student_daily_log',
metadata: { viewedStudentId: studentId },
created_at: new Date(),
});

res.json(logs);
} catch (err) {
res.status(500).json({ error: err.message });
}
}

return {
fetchSupportDailyLog,
};
};

module.exports = activityLogController;
27 changes: 27 additions & 0 deletions src/models/activityLog.js
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 2 additions & 1 deletion src/models/userTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ const mongoose = require('mongoose');
const userTaskSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
role: { type: String, enum: ['student', 'educator'], required: true },
role: { type: String, enum: ['student', 'educator','support'], required: true },
// Add 'support' role
});

module.exports = mongoose.model('UserTask', userTaskSchema, 'usertask');
11 changes: 11 additions & 0 deletions src/routes/activityLogRouter.js
Original file line number Diff line number Diff line change
@@ -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;
55 changes: 23 additions & 32 deletions src/startup/middleware.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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);
};
33 changes: 12 additions & 21 deletions src/startup/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ const educatorRouter = require('../routes/educatorRouter');
const atomRouter = require('../routes/atomRouter');
const intermediateTaskRouter = require('../routes/intermediateTaskRouter');
const savedFilterRouter = require('../routes/savedFilterRouter')(savedFilter);
const summaryDashboardRouter = require('../routes/summaryDashboard.routes');
// lbdashboard
const bidTermsRouter = require('../routes/lbdashboard/bidTermsRouter');
const bidsRouter = require('../routes/lbdashboard/bidsRouter');
Expand Down Expand Up @@ -416,26 +417,19 @@ const lessonPlanSubmissionRouter = require('../routes/lessonPlanner/lessonPlanSu
const epBadge = require('../models/educationPortal/badgeModel');
const studentBadges = require('../models/educationPortal/studentBadgesModel');
const badgeSystemRouter = require('../routes/educationPortal/badgeSystemRouter');

const promotionDetailsRouter = require('../routes/promotionDetailsRouter');
const activityLogRouter = require('../routes/activityLogRouter');
const jobHitsAndApplicationsRoutes = require('../routes/jobAnalyticsRouter');

const gardenRouter = require('../routes/kitchenInventory/gardenRouter')();
// Kitchen and Inventory portal routes
const kitchenInventoryRouter = require('../routes/kitchenandinventory/KIInventoryRouter')();
const summaryDashboardRouter = require('../routes/summaryDashboard.routes');

// Actual Cost
const actualCostRouter = require('../routes/actualCostRouter')();

const recipeRouter = require('../routes/kitchenInventory/recipeRouter')();

const jobHitsAndApplicationsRoutes = require('../routes/jobAnalytics/JobHitsAndApplicationsRoutes');
const actualCostRouter = require('../routes/actualCostRouter');
const kitchenInventoryRouter = require('../routes/kitchenandinventory/KIInventoryRouter');

// Education Portal
const educatorRoutes = require('../routes/educatorRoutes');

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);
Expand Down Expand Up @@ -635,23 +629,21 @@ module.exports = function (app) {
app.use('/api/lb', bidDeadlinesRouter);
app.use('/api/lb', SMSRouter);

// Education Portal
app.use('/api/educationportal/educator', educatorRoutes);
app.use('/api', materialCostRouter);

app.use('/api/educator/reports', studentReportRouter());
// education portal
app.use('/api/education', badgeSystemRouter);

app.use('/api/education', badgeSystemRouter);
app.use('/api/lp', lessonPlanSubmissionRouter);

app.use('/api/education', browsableLessonPlanRouter);

app.use('/api/educator/reports', downloadReportRouter);

// Kitchen and Inventory portal routes
app.use('/api/kitchenandinventory/inventory', kitchenInventoryRouter);
app.use('/api/kitchenandinventory/garden', gardenRouter);

// Need to implement gardenRouter
// app.use('/api/kitchenandinventory/garden', gardenRouter);

// Education Portal
app.use('/api/student/profile', educationProfileRouter);
Expand All @@ -660,7 +652,6 @@ module.exports = function (app) {

app.use('/api/lp', lessonPlanSubmissionRouter);

app.use('/api/kitchenandinventory/recipes', recipeRouter);

app.use('/api/analytics', analyticsRouter);
// Need to implement gardenRouter
// app.use('/api/kitchenandinventory/recipes', recipeRouter);
};
34 changes: 34 additions & 0 deletions src/utilities/jwtVerificationLogic.js
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 9 additions & 7 deletions src/utilities/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,22 @@ const normalizeRequestor = async (requestor) => {
return role ? { ...requestor, requestorId, role } : null;
};

// req.body -> requestor, 'fetchSupportDailyLog -> action'
const hasPermission = async (requestor, action) => {
const normalizedRequestor = await normalizeRequestor(requestor);

if (!normalizedRequestor) {
return false;
}

const defaultRemoved =
normalizedRequestor.requestorId &&
(await hasDefaultPermissionRemoved(normalizedRequestor.requestorId, action));
const roleHasPermission = await hasRolePermission(normalizedRequestor.role, action);
const individualHasPermission =
normalizedRequestor.requestorId &&
(await hasIndividualPermission(normalizedRequestor.requestorId, action));
// Extract from normalizedRequestor
const { requestorId, role } = normalizedRequestor;

const defaultRemoved = await hasDefaultPermissionRemoved(requestorId, action);

const roleHasPermission = await hasRolePermission(role, action);

const individualHasPermission = await hasIndividualPermission(requestorId, action);

return (!defaultRemoved && roleHasPermission) || individualHasPermission;
};
Expand Down
Loading
Loading