Skip to content

Commit 29b0e97

Browse files
Merge pull request #1898 from OneCommunityGlobal/kristin-add-support-team-backend
Sai Sandeep taking over for Kristin: Phase 4 - Support Team Backend (Permissions + Daily Log Access)
2 parents 541a564 + 7995a41 commit 29b0e97

13 files changed

Lines changed: 13121 additions & 126 deletions

package-lock.json

Lines changed: 1986 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"test:ci": "npm test -- --coverage",
2323
"lint": "eslint ./src --ext .js,.jsx --max-warnings=2000 --ignore-pattern '**/node_modules/**' --ignore-pattern '**/coverage/**' --ignore-pattern '**/dist/**' --ignore-pattern '**/build/**'",
2424
"lint:fix": "eslint ./src --ext .js,.jsx --fix --ignore-pattern '**/node_modules/**' --ignore-pattern '**/coverage/**' --ignore-pattern '**/dist/**' --ignore-pattern '**/build/**'",
25-
"build": "babel src -d dist && mkdir -p dist/data && cp -r src/data/* dist/data/ 2>/dev/null || true",
25+
"build": "babel src -d dist && shx mkdir -p dist/data && shx cp -r src/data/* dist/data/",
2626
"buildw": "babel src -d dist --watch",
2727
"start": "node dist/server.js",
2828
"dev": "nodemon --exec \"babel-node --only src src/server.js\"",
@@ -60,6 +60,7 @@
6060
"pidtree": "^0.6.0",
6161
"prettier": "3.2.5",
6262
"puppeteer": "^24.40.0",
63+
"shx": "^0.4.0",
6364
"supertest": "^6.3.4"
6465
},
6566
"dependencies": {
@@ -87,7 +88,7 @@
8788
"body-parser": "^1.20.4",
8889
"card-validator": "^10.0.2",
8990
"cheerio": "^0.22.0",
90-
"cloudinary": "^2.8.0",
91+
"cloudinary": "^2.9.0",
9192
"compression": "^1.8.0",
9293
"cors": "^2.8.4",
9394
"cron": "^1.8.2",

src/app.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,30 @@ const logger = require('./startup/logger');
77
const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler');
88
// const experienceRoutes = require('./routes/applicantAnalyticsRoutes');
99

10+
// 1. Core initialization
1011
logger.init();
11-
1212
app.use(Sentry.Handlers.requestHandler());
1313

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

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

2224
const helpFeedbackRouter = require('./routes/helpFeedbackRouter');
2325
const helpRequestRouter = require('./routes/helpRequestRouter');
24-
2526
app.use('/api/feedback', helpFeedbackRouter);
2627
app.use('/api/helprequest', helpRequestRouter);
2728

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

30-
const weeklyReportsRouter = require('./routes/weeklyReportsRouter');
31-
32-
app.use('/api', weeklyReportsRouter);
33-
34-
// ⚠ This must come *after* your custom /api routes
35-
require('./startup/routes')(app);
36-
33+
// 4. Error Handling (The "Safety Net")
3734
app.use(Sentry.Handlers.errorHandler());
3835
app.use(globalErrorHandler);
3936

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const ActivityLog = require('../models/activityLog');
2+
const UserProfile = require('../models/userProfile');
3+
const { hasPermission } = require('../utilities/permissions');
4+
5+
const activityLogController = function () {
6+
async function fetchSupportDailyLog(req, res) {
7+
try {
8+
const { studentId } = req.params;
9+
const { requestor } = req.body;
10+
11+
if (!studentId) return res.status(400).json({ error: 'Missing studentId' });
12+
13+
if (!(await hasPermission(requestor, 'fetchSupportDailyLog'))) {
14+
return res
15+
.status(403)
16+
.json({ error: 'Forbidden: Only support role can access this endpoint' });
17+
}
18+
19+
const studentProfile = await UserProfile.findById(studentId).select('orgId');
20+
if (!studentProfile) {
21+
return res.status(404).json({ error: 'Student not found' });
22+
}
23+
24+
if (String(studentProfile.orgId) !== String(requestor.orgId)) {
25+
return res
26+
.status(403)
27+
.json({ error: 'Forbidden: Cannot access student outside your organization' });
28+
}
29+
30+
// fetch logs
31+
const logs = await ActivityLog.find({ actor_id: studentId })
32+
.sort({ created_at: -1 })
33+
.select('action_type metadata created_at actor_id');
34+
35+
await ActivityLog.create({
36+
actor_id: requestor.requestorId,
37+
action_type: 'view_student_daily_log',
38+
metadata: { viewedStudentId: studentId },
39+
created_at: new Date(),
40+
});
41+
42+
res.json(logs);
43+
} catch (err) {
44+
res.status(500).json({ error: err.message });
45+
}
46+
}
47+
48+
return {
49+
fetchSupportDailyLog,
50+
};
51+
};
52+
53+
module.exports = activityLogController;

src/models/activityLog.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const mongoose = require('mongoose');
2+
3+
const ActivityLogSchema = new mongoose.Schema(
4+
{
5+
actor_id: {
6+
type: mongoose.Schema.Types.ObjectId,
7+
ref: 'UserProfile',
8+
required: true,
9+
},
10+
action_type: {
11+
type: String,
12+
required: true,
13+
},
14+
metadata: {
15+
type: mongoose.Schema.Types.Mixed,
16+
default: {},
17+
},
18+
},
19+
{
20+
timestamps: {
21+
createdAt: 'created_at',
22+
updatedAt: false,
23+
},
24+
},
25+
);
26+
27+
module.exports = mongoose.model('ActivityLog', ActivityLogSchema);

src/models/userTask.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ const mongoose = require('mongoose');
33
const userTaskSchema = new mongoose.Schema({
44
name: { type: String, required: true },
55
email: { type: String, required: true, unique: true },
6-
role: { type: String, enum: ['student', 'educator'], required: true },
6+
role: { type: String, enum: ['student', 'educator','support'], required: true },
7+
// Add 'support' role
78
});
89

910
module.exports = mongoose.model('UserTask', userTaskSchema, 'usertask');

src/routes/activityLogRouter.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const express = require('express');
2+
const activityLogRouter = express.Router();
3+
// Invoke the controller factory
4+
const controller = require('../controllers/activityLogController')();
5+
6+
activityLogRouter
7+
.route('/:studentId')
8+
.get(controller.fetchSupportDailyLog);
9+
10+
// Export the INSTANCE
11+
module.exports = activityLogRouter;

src/startup/middleware.js

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/* eslint-disable complexity */
22
/* eslint-disable no-magic-numbers */
3-
const jwt = require('jsonwebtoken');
4-
const moment = require('moment');
3+
54
const express = require('express');
6-
const config = require('../config');
75
const webhookController = require('../controllers/lbdashboard/webhookController'); // your new controller
86
const { Bids } = require('../models/lbdashboard/bids'); // or wherever you're getting Bids
97

108
const { webhookTest } = webhookController(Bids);
119

10+
const jwtVerificationLogic = require('../utilities/jwtVerificationLogic');
11+
1212
const paypalAuthMiddleware = (req, res, next) => {
1313
const authHeader = req.header('Paypal-Auth-Algo');
1414
if (!authHeader) {
@@ -123,40 +123,31 @@ module.exports = function (app) {
123123
if (openPaths.includes(req.path)) {
124124
return next(); // Allow PayPal requests through
125125
}
126-
if (!req.header('Authorization')) {
127-
res.status(401).send({ 'error:': 'Unauthorized request' });
128-
return;
129-
}
130-
const authToken = req.header(config.REQUEST_AUTHKEY);
131126

132-
let payload = '';
127+
// HEADER EXTRACTION
128+
const authHeader = req.header('Authorization');
129+
const payload = jwtVerificationLogic(authHeader, res);
133130

134-
try {
135-
payload = jwt.verify(authToken, config.JWT_SECRET);
136-
} catch (error) {
137-
res.status(401).send('Invalid token');
138-
return;
139-
}
140-
if (
141-
!payload ||
142-
!payload.expiryTimestamp ||
143-
!payload.userid ||
144-
!payload.role ||
145-
moment().isAfter(payload.expiryTimestamp)
146-
) {
147-
res.status(401).send('Unauthorized request');
148-
return;
149-
}
131+
// FIX: If payload is a response object (meaning logic already sent a 401), STOP HERE.
132+
if (res.headersSent) return;
133+
134+
// ATTACH DATA & CONTINUE
135+
// Now we know payload is the valid decoded token
136+
const requestor = {
137+
requestorId: payload.userid,
138+
role: payload.role,
139+
permissions: payload.permissions,
140+
};
150141

151-
const requestor = {};
152-
requestor.requestorId = payload.userid;
153-
requestor.role = payload.role;
154-
requestor.permissions = payload.permissions;
142+
req.user = requestor;
155143

156-
req.body.requestor = requestor;
157-
next();
144+
if (req.body) {
145+
req.body.requestor = requestor;
146+
}
147+
148+
return next();
158149
});
159150

160-
// Apply PayPal middleware only to specific route
151+
// PROTECTED ROUTES
161152
app.post('/api/lb/myWebhooks/', paypalAuthMiddleware, webhookTest);
162153
};

src/startup/routes.js

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ const educatorRouter = require('../routes/educatorRouter');
379379
const atomRouter = require('../routes/atomRouter');
380380
const intermediateTaskRouter = require('../routes/intermediateTaskRouter');
381381
const savedFilterRouter = require('../routes/savedFilterRouter')(savedFilter);
382+
const summaryDashboardRouter = require('../routes/summaryDashboard.routes');
382383
// lbdashboard
383384
const bidTermsRouter = require('../routes/lbdashboard/bidTermsRouter');
384385
const bidsRouter = require('../routes/lbdashboard/bidsRouter');
@@ -416,26 +417,19 @@ const lessonPlanSubmissionRouter = require('../routes/lessonPlanner/lessonPlanSu
416417
const epBadge = require('../models/educationPortal/badgeModel');
417418
const studentBadges = require('../models/educationPortal/studentBadgesModel');
418419
const badgeSystemRouter = require('../routes/educationPortal/badgeSystemRouter');
419-
420420
const promotionDetailsRouter = require('../routes/promotionDetailsRouter');
421+
const activityLogRouter = require('../routes/activityLogRouter');
422+
const jobHitsAndApplicationsRoutes = require('../routes/jobAnalyticsRouter');
421423

422-
const gardenRouter = require('../routes/kitchenInventory/gardenRouter')();
423-
// Kitchen and Inventory portal routes
424-
const kitchenInventoryRouter = require('../routes/kitchenandinventory/KIInventoryRouter')();
425-
const summaryDashboardRouter = require('../routes/summaryDashboard.routes');
426-
427-
// Actual Cost
428-
const actualCostRouter = require('../routes/actualCostRouter')();
429-
430-
const recipeRouter = require('../routes/kitchenInventory/recipeRouter')();
431-
432-
const jobHitsAndApplicationsRoutes = require('../routes/jobAnalytics/JobHitsAndApplicationsRoutes');
424+
const actualCostRouter = require('../routes/actualCostRouter');
425+
const kitchenInventoryRouter = require('../routes/kitchenandinventory/KIInventoryRouter');
433426

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

437430
module.exports = function (app) {
438431
app.use('/api/bm/summary-dashboard', summaryDashboardRouter);
432+
app.use('/api/support/daily-log', activityLogRouter);
439433
app.use('/api', forgotPwdRouter);
440434
app.use('/api', loginRouter);
441435
app.use('/api', forcePwdRouter);
@@ -635,23 +629,21 @@ module.exports = function (app) {
635629
app.use('/api/lb', bidDeadlinesRouter);
636630
app.use('/api/lb', SMSRouter);
637631

638-
// Education Portal
639-
app.use('/api/educationportal/educator', educatorRoutes);
640632
app.use('/api', materialCostRouter);
641633

642634
app.use('/api/educator/reports', studentReportRouter());
643635
// education portal
644-
app.use('/api/education', badgeSystemRouter);
645636

637+
app.use('/api/education', badgeSystemRouter);
646638
app.use('/api/lp', lessonPlanSubmissionRouter);
647-
648639
app.use('/api/education', browsableLessonPlanRouter);
649-
650640
app.use('/api/educator/reports', downloadReportRouter);
651641

652642
// Kitchen and Inventory portal routes
653643
app.use('/api/kitchenandinventory/inventory', kitchenInventoryRouter);
654-
app.use('/api/kitchenandinventory/garden', gardenRouter);
644+
645+
// Need to implement gardenRouter
646+
// app.use('/api/kitchenandinventory/garden', gardenRouter);
655647

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

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

663-
app.use('/api/kitchenandinventory/recipes', recipeRouter);
664-
665-
app.use('/api/analytics', analyticsRouter);
655+
// Need to implement gardenRouter
656+
// app.use('/api/kitchenandinventory/recipes', recipeRouter);
666657
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const moment = require('moment');
2+
const config = require('../config');
3+
const jwt = require('jsonwebtoken');
4+
5+
const jwtVerificationLogic = (authHeader, res) => {
6+
if (!authHeader) {
7+
return res.status(401).json({ error: 'Unauthorized request: No header' });
8+
}
9+
10+
let authToken = authHeader;
11+
// If it has Bearer, strip it. If not, use it as is.
12+
if (authHeader.startsWith('Bearer ')) {
13+
[, authToken] = authHeader.split(' ');
14+
}
15+
16+
let payload;
17+
try {
18+
payload = jwt.verify(authToken, config.JWT_SECRET);
19+
} catch (error) {
20+
return res.status(401).json({ error: 'Invalid token', details: error.message });
21+
}
22+
23+
// Ensure mock date in test is not in the past
24+
const isExpired = moment().isAfter(payload.expiryTimestamp);
25+
if (!payload.userid || !payload.role || isExpired) {
26+
// eslint-disable-next-line no-console
27+
console.log('Auth failed. Expiry:', payload.expiryTimestamp);
28+
return res.status(401).send('Unauthorized request: Token expired or invalid payload');
29+
}
30+
31+
return payload;
32+
};
33+
34+
module.exports = jwtVerificationLogic;

0 commit comments

Comments
 (0)