diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 3c508cc9..d397f3e5 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -53,6 +53,7 @@ jobs:
docker run -d \
--name backend_test \
-e MONGODB_URI=${{ secrets.MONGODB_URI }} \
+ -e SESSION_SECRET=${{ secrets.SESSION_SECRET }} \
-e JWT_SECRET_TOKEN=${{ secrets.JWT_SECRET_TOKEN }} \
-e FRONTEND_URL=${{ secrets.FRONTEND_URL }} \
-e BACKEND_URL=${{ secrets.BACKEND_URL }} \
@@ -62,5 +63,7 @@ jobs:
sleep 15
echo "Checking backend container logs:"
docker logs backend_test
+ curl --fail http://localhost:8000/ || (echo "Backend container failed to respond!" && docker logs backend_test && exit 1)
+ docker stop backend_test
- name: Push Backend Docker image
run: docker push ${{ secrets.DOCKER_USERNAME }}/backend:latest
diff --git a/.gitignore b/.gitignore
index d042671c..48296bbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,9 @@
/build
frontend/build/
+# generated by the tailwind build step
+frontend/src/tailwind.css
+
# new production logs
production.log
diff --git a/README.md b/README.md
index e4ca18f7..0aa2b385 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ cd Student_Database_COSA
# For Windows
copy .env.example .env
```
- Now, open the new `.env` file and fill in your actual values.
+ Now, open the new `.env` file and fill in your actual values. `SESSION_SECRET` and `JWT_SECRET_TOKEN` are required — the server refuses to start without them.
- **Seed the database:**
This next command populates the database with initial necessary data. **You only need to run this once during the initial setup.**
@@ -78,7 +78,7 @@ cd Student_Database_COSA
```bash
node index.js
```
-The backend server should now be running on `http://localhost:5000`.Keep this terminal open.
+The backend server should now be running on `http://localhost:8000`.Keep this terminal open.
### 3. Frontend Setup
**Open a new, separate terminal window.** This is important, as your backend server needs to keep running in the first terminal.
diff --git a/backend/.env.example b/backend/.env.example
index d0a82c30..61adbf09 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -1,8 +1,8 @@
MONGODB_URI = mongodb://localhost:27017/cosadatabase
JWT_SECRET_TOKEN='secret-token'
FRONTEND_URL=http://localhost:3000
-BACKEND_URL=http://localhost:5000
-PORT=5000
+BACKEND_URL=http://localhost:8000
+PORT=8000
GOOGLE_CLIENT_ID=OAuth_Client_ID_from_google_cloud_console
GOOGLE_CLIENT_SECRET=OAuth_Client_Secret_from_google_cloud_console
diff --git a/backend/Dockerfile b/backend/Dockerfile
index b8434627..2f99e125 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -6,6 +6,9 @@ RUN apk update && apk upgrade --no-cache
COPY package.json package-lock.json ./
+# Puppeteer's Chrome is installed in the runtime stage via apk.
+ENV PUPPETEER_SKIP_DOWNLOAD=true
+
RUN npm ci
COPY . .
@@ -17,7 +20,9 @@ FROM node:18-alpine
WORKDIR /app
-RUN apk update && apk upgrade --no-cache
+RUN apk update && apk upgrade --no-cache \
+ && apk add --no-cache chromium \
+ && rm -rf /var/cache/apk/*
COPY --from=builder /app ./
@@ -25,7 +30,10 @@ RUN addgroup -S appgroup && adduser -S appuser -G appgroup && chown -R appuser:a
USER appuser
-EXPOSE 3000
+ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
+ENV PUPPETEER_NO_SANDBOX=true
+
+EXPOSE 8000
CMD ["node", "index.js"]
diff --git a/backend/controllers/achievementController.js b/backend/controllers/achievementController.js
index b46ff2e1..0478b88d 100644
--- a/backend/controllers/achievementController.js
+++ b/backend/controllers/achievementController.js
@@ -1,5 +1,6 @@
const { Achievement } = require("../models/schema");
const { v4: uuidv4 } = require("uuid");
+const { ROLE_GROUPS } = require("../utils/roles");
// GET unverified achievements by type
const getUnendorsedAchievements = async (req, res) => {
@@ -94,7 +95,10 @@ const addAchievement = async (req, res) => {
user_id,
} = req.body;
- if (!title || !category || !date_achieved || !user_id) {
+ const isAdmin = ROLE_GROUPS.ADMIN.includes(req.user.role);
+ const targetUserId = isAdmin ? user_id : req.user._id;
+
+ if (!title || !category || !date_achieved || !targetUserId) {
return res.status(400).json({
message: "Missing required fields",
});
@@ -102,7 +106,7 @@ const addAchievement = async (req, res) => {
const achievement = new Achievement({
achievement_id: uuidv4(),
- user_id,
+ user_id: targetUserId,
title,
description,
category,
diff --git a/backend/controllers/certificateBatchController.js b/backend/controllers/certificateBatchController.js
index f6f7fab4..2e838501 100644
--- a/backend/controllers/certificateBatchController.js
+++ b/backend/controllers/certificateBatchController.js
@@ -9,7 +9,6 @@ const { findEvent } = require("../services/event.service");
const { findTemplate } = require("../services/template.service");
const { getApprovers } = require("../services/user.service");
const {
- getOrganization,
getCoordinatorOrganization,
} = require("../services/organization.service");
const { HttpError } = require("../utils/httpError");
@@ -107,7 +106,7 @@ async function createBatch(req, res) {
}
- const newBatch = await CertificateBatch.create({
+ await CertificateBatch.create({
title,
eventId: event._id,
templateId: template._id,
@@ -188,6 +187,12 @@ async function editBatch(req, res) {
return res.status(404).json({ message: "Batch not found" });
}
+ if (batch.initiatedBy.toString() !== id) {
+ return res.status(403).json({
+ message: "You are not authorized to edit this batch",
+ });
+ }
+
Object.assign(batch, validation.data);
batch.lifecycleStatus = action;
@@ -271,7 +276,7 @@ async function duplicateBatch(req, res) {
const array = batch.title.split("(Copy)");
const count = array.length -1;
const title = `${array[0]} Copy(${count})`;
- const newBatch = await CertificateBatch.create({
+ await CertificateBatch.create({
...batch.toObject(),
title: title,
lifecycleStatus: "Draft",
@@ -539,6 +544,13 @@ async function approveBatch(req, res) {
};
}
+ // Final (President) approval: generate certificates BEFORE marking the
+ // batch Active/Approved. If generation fails, the batch stays Submitted
+ // so the approval can be retried once the cause is fixed.
+ if (level === 1) {
+ await generateCertificates(batch);
+ }
+
const updatedBatch = await CertificateBatch.findOneAndUpdate(
matchQuery,
update,
@@ -552,17 +564,11 @@ async function approveBatch(req, res) {
});
}
- if (level === 1) {
- // Final (President) approval just happened - generate certificates
- // exactly once, from the freshly-updated, level===2 document.
- await generateCertificates(updatedBatch);
- }
-
return res.status(200).json({
message:
level === 0
? "Batch approved by GENSEC. Forwarded to President."
- : "Batch approved successfully. Certificates are being generated.",
+ : "Batch approved successfully. Certificates generated.",
});
} catch (err) {
if (err instanceof HttpError) {
diff --git a/backend/controllers/eventControllers.js b/backend/controllers/eventControllers.js
index bf2edf17..22ff146d 100644
--- a/backend/controllers/eventControllers.js
+++ b/backend/controllers/eventControllers.js
@@ -215,7 +215,30 @@ exports.deleteEvent = async (req, res) => {
exports.updateEvent = async (req, res) => {
try {
const { eventId } = req.params;
- const updates = req.body;
+
+ const allowedFields = [
+ "title",
+ "description",
+ "category",
+ "type",
+ "schedule",
+ "registration",
+ "budget",
+ "status",
+ ];
+
+ const updates = {};
+ for (const field of allowedFields) {
+ if (req.body[field] !== undefined) {
+ updates[field] = req.body[field];
+ }
+ }
+
+ if (Object.keys(updates).length === 0) {
+ return res.status(400).json({
+ message: "No editable fields provided",
+ });
+ }
const event = await Event.findByIdAndUpdate(
eventId,
@@ -241,7 +264,6 @@ exports.updateEvent = async (req, res) => {
return res.status(500).json({
message: "Server error",
- error: err.message,
});
}
};
@@ -450,7 +472,7 @@ exports.registerForEvent = async (req, res) => {
exports.getEventsByRole = async (req, res) => {
- const userRole = req.params.userRole;
+ const userRole = req.user.role;
try {
let query = {};
@@ -465,7 +487,7 @@ exports.getEventsByRole = async (req, res) => {
break;
case "CLUB_COORDINATOR": {
- const username = req.query.username;
+ const username = req.user.username;
if (!username) {
return res.status(400).json({
diff --git a/backend/controllers/feedbackController.js b/backend/controllers/feedbackController.js
index c93cd8cc..4171edeb 100644
--- a/backend/controllers/feedbackController.js
+++ b/backend/controllers/feedbackController.js
@@ -15,13 +15,12 @@ exports.addFeedback = async (req, res) => {
type,
target_type,
target_id,
- feedback_by,
rating,
comments,
is_anonymous,
} = req.body;
- if (!type || !target_type || !target_id || !feedback_by) {
+ if (!type || !target_type || !target_id) {
return res.status(400).json({
message: "Missing required fields",
});
@@ -47,7 +46,7 @@ exports.addFeedback = async (req, res) => {
type,
target_type,
target_id,
- feedback_by,
+ feedback_by: req.user._id,
rating,
comments,
is_anonymous:
@@ -225,6 +224,11 @@ exports.viewFeedback = async (req, res) => {
}
const fbObj = fb.toObject();
+
+ if (fbObj.is_anonymous) {
+ fbObj.feedback_by = null;
+ }
+
fbObj.target_data = targetData;
return fbObj;
diff --git a/backend/controllers/profileController.js b/backend/controllers/profileController.js
index 2fe38e0e..2217e5d9 100644
--- a/backend/controllers/profileController.js
+++ b/backend/controllers/profileController.js
@@ -11,12 +11,7 @@ cloudinary.config({
exports.updateProfilePhoto = async (req, res) => {
try {
- const { ID_No } = req.body;
- if (!ID_No) {
- return res.status(400).json({ error: "ID_No is required" });
- }
-
- const user = await User.findOne({ user_id: ID_No });
+ const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
@@ -69,12 +64,7 @@ exports.updateProfilePhoto = async (req, res) => {
// Delete profile photo (reset to default)
exports.deleteProfilePhoto = async (req, res) => {
try {
- const { ID_No } = req.query; // Get ID_No from frontend for DELETE
- if (!ID_No) {
- return res.status(400).json({ error: "ID_No is required" });
- }
-
- const user = await User.findOne({ user_id: ID_No }); // Capital User
+ const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
@@ -116,6 +106,13 @@ exports.updateStudentProfile = async (req, res) => {
.json({ success: false, message: "Student not found" });
}
+ if (user._id.toString() !== req.user.id.toString()) {
+ return res.status(403).json({
+ success: false,
+ message: "Not authorized to update this profile",
+ });
+ }
+
// ---------- PERSONAL INFO ----------
if (updatedDetails.personal_info) {
const {
@@ -125,7 +122,6 @@ exports.updateStudentProfile = async (req, res) => {
gender,
date_of_birth,
profilePic,
- cloudinaryUrl,
} = updatedDetails.personal_info;
if (name) {
@@ -146,9 +142,6 @@ exports.updateStudentProfile = async (req, res) => {
if (profilePic) {
user.personal_info.profilePic = profilePic;
}
- if (cloudinaryUrl) {
- user.personal_info.cloudinaryUrl = cloudinaryUrl;
- }
}
// ---------- ACADEMIC INFO ----------
diff --git a/backend/controllers/skillController.js b/backend/controllers/skillController.js
index 7a0ef511..3604251e 100644
--- a/backend/controllers/skillController.js
+++ b/backend/controllers/skillController.js
@@ -1,5 +1,6 @@
const { UserSkill, Skill } = require("../models/schema");
const { v4: uuidv4 } = require("uuid");
+const { ROLE_GROUPS } = require("../utils/roles");
// GET unendorsed user skills for a particular skill type
exports.getUnendorsedUserSkills = async (req, res) => {
@@ -176,8 +177,11 @@ exports.createUserSkill = async (req, res) => {
try {
const { user_id, skill_id, proficiency_level, position_id } = req.body;
+ const isAdmin = ROLE_GROUPS.ADMIN.includes(req.user.role);
+ const targetUserId = isAdmin ? user_id : req.user._id;
+
const newUserSkill = new UserSkill({
- user_id,
+ user_id: targetUserId,
skill_id,
proficiency_level,
position_id: position_id || null,
diff --git a/backend/index.js b/backend/index.js
index abe27462..ca185910 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -30,9 +30,16 @@ const taskRoutes = require("./routes/task.routes.js");
const studentsRoutes = require("./routes/students.js");
+const MongoDBStore = require("connect-mongodb-session")(session);
+
const app = express();
-if (process.env.NODE_ENV === "production") {
+const isHostedOverHttps =
+ (process.env.FRONTEND_URL || "").startsWith("https://") ||
+ process.env.NODE_ENV === "production";
+
+if (isHostedOverHttps) {
+ // Required behind Render's proxy, otherwise `secure` cookies are never sent.
app.set("trust proxy", 1);
}
@@ -47,14 +54,29 @@ if (!process.env.SESSION_SECRET) {
throw new Error("SESSION_SECRET environment variable is required");
}
+if (!process.env.JWT_SECRET_TOKEN) {
+ throw new Error("JWT_SECRET_TOKEN environment variable is required");
+}
+
+const sessionStore = new MongoDBStore({
+ uri: process.env.MONGODB_URI,
+ collection: "sessions",
+});
+
+sessionStore.on("error", (error) => {
+ console.error("Session store error:", error);
+});
+
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
+ store: sessionStore,
cookie: {
- secure: process.env.NODE_ENV === "production", // HTTPS only in prod
- sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // cross-origin in prod,
+ secure: isHostedOverHttps, // HTTPS only when hosted
+ sameSite: isHostedOverHttps ? "none" : "lax", // cross-site when hosted
+ maxAge: 7 * 24 * 60 * 60 * 1000,
},
}),
);
@@ -82,7 +104,6 @@ app.use("/api/positions", positionsRoutes);
app.use("/api/orgUnit", organizationalUnitRoutes);
app.use("/api/announcements", announcementRoutes);
app.use("/api/dashboard", dashboardRoutes);
-app.use("/api/announcements", announcementRoutes);
app.use("/api/analytics", analyticsRoutes);
app.use("/api/rooms", roomBookingRoutes);
app.use("/api/por", porRoutes);
diff --git a/backend/models/passportConfig.js b/backend/models/passportConfig.js
index 82cb533f..fccffb20 100644
--- a/backend/models/passportConfig.js
+++ b/backend/models/passportConfig.js
@@ -14,52 +14,58 @@ passport.use(
);
// Google OAuth Strategy
-passport.use(
- new GoogleStrategy(
- {
- clientID: process.env.GOOGLE_CLIENT_ID,
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
- callbackURL: `${process.env.BACKEND_URL}/auth/google/verify`, // Update with your callback URL
- },
- async (accessToken, refreshToken, profile, done) => {
- // Check if the user already exists in your database
- if (!isIITBhilaiEmail(profile.emails[0].value)) {
- console.log("Google OAuth blocked for: ", profile.emails[0].value);
- return done(null, false, {
- message: "Only @iitbhilai.ac.in emails are allowed.",
- });
- }
- try {
- const user = await User.findOne({ username: profile.emails[0].value });
-
- if (user) {
- // If user exists, return the user
- return done(null, user);
+if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
+ passport.use(
+ new GoogleStrategy(
+ {
+ clientID: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ callbackURL: `${process.env.BACKEND_URL}/auth/google/verify`, // Update with your callback URL
+ },
+ async (accessToken, refreshToken, profile, done) => {
+ // Check if the user already exists in your database
+ if (!isIITBhilaiEmail(profile.emails[0].value)) {
+ console.log("Google OAuth blocked for: ", profile.emails[0].value);
+ return done(null, false, {
+ message: "Only @iitbhilai.ac.in emails are allowed.",
+ });
}
- // If user doesn't exist, create a new user in your database
- const newUser = new User({
- username: profile.emails[0].value,
- role: "STUDENT",
- strategy: "google",
- personal_info: {
- name: profile.displayName || "No Name",
- email: profile.emails[0].value,
- profilePic:
- profile.photos && profile.photos.length > 0
- ? profile.photos[0].value
- : "https://www.gravatar.com/avatar/?d=mp",
- },
- onboardingComplete: false,
- });
+ try {
+ const user = await User.findOne({ username: profile.emails[0].value });
- await newUser.save();
- return done(null, newUser);
- } catch (error) {
- return done(error);
- }
- },
- ),
-);
+ if (user) {
+ // If user exists, return the user
+ return done(null, user);
+ }
+ // If user doesn't exist, create a new user in your database
+ const newUser = new User({
+ username: profile.emails[0].value,
+ role: "STUDENT",
+ strategy: "google",
+ personal_info: {
+ name: profile.displayName || "No Name",
+ email: profile.emails[0].value,
+ profilePic:
+ profile.photos && profile.photos.length > 0
+ ? profile.photos[0].value
+ : "https://www.gravatar.com/avatar/?d=mp",
+ },
+ onboardingComplete: false,
+ });
+
+ await newUser.save();
+ return done(null, newUser);
+ } catch (error) {
+ return done(error);
+ }
+ },
+ ),
+ );
+} else {
+ console.warn(
+ "Google OAuth strategy disabled: GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET not set.",
+ );
+}
passport.serializeUser((user, done) => {
done(null, user);
diff --git a/backend/models/schema.js b/backend/models/schema.js
index 4211276e..7c224c41 100644
--- a/backend/models/schema.js
+++ b/backend/models/schema.js
@@ -109,6 +109,15 @@ userSchema.index(
userSchema.plugin(passportLocalMongoose);
userSchema.plugin(findOrCreate);
+// Never serialize the password hash/salt to API clients.
+userSchema.set("toJSON", {
+ transform: (_doc, ret) => {
+ delete ret.salt;
+ delete ret.hash;
+ return ret;
+ },
+});
+
//organizational unit
const organizationalUnitSchema = new mongoose.Schema({
unit_id: {
diff --git a/backend/package.json b/backend/package.json
index 115d9185..657b043e 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -3,12 +3,16 @@
"version": "1.0.0",
"description": "This is the backend for the cosa database GUI",
"main": "index.js",
+ "engines": {
+ "node": ">=18"
+ },
"scripts": {
"test": "jest --runInBand",
+ "lint": "eslint .",
"prepare": "node -e \"if(process.env.NODE_ENV !== 'production'){process.exit(1)}\" || (cd .. && husky install)",
"start": "node index.js",
"dev": "nodemon index.js",
- "build": "nodemon build.js"
+ "build": "node --check index.js"
},
"lint-staged": {
"*.js": [
@@ -23,7 +27,26 @@
],
"env": {
"node": true,
- "es6": true
+ "es6": true,
+ "jest": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 2022
+ },
+ "rules": {
+ "node/no-unpublished-require": [
+ "error",
+ {
+ "allowModules": [
+ "supertest",
+ "mongodb-memory-server"
+ ]
+ }
+ ],
+ "node/no-unsupported-features/es-syntax": [
+ "error",
+ { "ignores": ["dynamicImport"] }
+ ]
}
},
"author": "",
diff --git a/backend/routes/analytics.js b/backend/routes/analytics.js
index bf31fc49..81b4af1d 100644
--- a/backend/routes/analytics.js
+++ b/backend/routes/analytics.js
@@ -12,7 +12,7 @@ router.get('/president', isAuthenticated, authorizeRole(['PRESIDENT']), controll
router.get('/gensec', isAuthenticated,authorizeRole([...ROLE_GROUPS.GENSECS]), controller.getGensecAnalytics);
// Route to get analytics for club coordinators
-router.get('/club-coordinator',authorizeRole(['CLUB_COORDINATOR']), isAuthenticated, controller.getClubCoordinatorAnalytics);
+router.get('/club-coordinator', isAuthenticated, authorizeRole(['CLUB_COORDINATOR']), controller.getClubCoordinatorAnalytics);
// Route to get analytics for students
router.get('/student', isAuthenticated,authorizeRole(['STUDENT']), controller.getStudentAnalytics);
diff --git a/backend/routes/events.js b/backend/routes/events.js
index fdad2ade..77ffe442 100644
--- a/backend/routes/events.js
+++ b/backend/routes/events.js
@@ -1,7 +1,5 @@
const express = require("express");
const router = express.Router();
-const { Event, User, OrganizationalUnit } = require("../models/schema");
-const { v4: uuidv4 } = require("uuid");
const isAuthenticated = require("../middlewares/isAuthenticated");
const isEventContact = require("../middlewares/isEventContact");
const authorizeRole = require("../middlewares/authorizeRole");
diff --git a/backend/routes/feedbackRoutes.js b/backend/routes/feedbackRoutes.js
index 00338d78..7d049166 100644
--- a/backend/routes/feedbackRoutes.js
+++ b/backend/routes/feedbackRoutes.js
@@ -12,7 +12,7 @@ router.post("/add",isAuthenticated, feedbackController.addFeedback);
router.get("/get-targetid",isAuthenticated, feedbackController.getTargetIds);
-router.get("/view-feedback", feedbackController.viewFeedback);
+router.get("/view-feedback",isAuthenticated,authorizeRole(ROLE_GROUPS.ADMIN), feedbackController.viewFeedback);
// requires user middleware that attaches user info to req.user
router.put("/mark-resolved/:id",isAuthenticated,authorizeRole(ROLE_GROUPS.ADMIN), feedbackController.markResolved);
diff --git a/backend/routes/onboarding.js b/backend/routes/onboarding.js
index 73cd3687..1af4ca29 100644
--- a/backend/routes/onboarding.js
+++ b/backend/routes/onboarding.js
@@ -1,6 +1,5 @@
const express = require("express");
const router = express.Router();
-const { User } = require("../models/schema");
const isAuthenticated = require("../middlewares/isAuthenticated");
const onboardingController = require(
diff --git a/backend/routes/orgUnit.js b/backend/routes/orgUnit.js
index 263c18e1..7fff0465 100644
--- a/backend/routes/orgUnit.js
+++ b/backend/routes/orgUnit.js
@@ -1,17 +1,6 @@
// routes/club.js
const express = require("express");
const router = express.Router();
-const mongoose = require("mongoose");
-const { v4: uuidv4 } = require("uuid");
-const {
- OrganizationalUnit,
- Event,
- Position,
- PositionHolder,
- Achievement,
- Feedback,
- User,
-} = require("../models/schema");
const isAuthenticated = require("../middlewares/isAuthenticated");
const authorizeRole = require("../middlewares/authorizeRole");
const { ROLE_GROUPS } = require("../utils/roles");
diff --git a/backend/routes/positionRoutes.js b/backend/routes/positionRoutes.js
index 86059919..78ff60d9 100644
--- a/backend/routes/positionRoutes.js
+++ b/backend/routes/positionRoutes.js
@@ -1,13 +1,20 @@
const express = require("express");
const router = express.Router();
const isAuthenticated = require("../middlewares/isAuthenticated");
+const authorizeRole = require("../middlewares/authorizeRole");
+const { ROLE_GROUPS } = require("../utils/roles");
const positionController = require(
"../controllers/positionController"
);
-// POST for adding a new position
-router.post("/add-position", isAuthenticated, positionController.addPosition);
+// POST for adding a new position (admin roles only)
+router.post(
+ "/add-position",
+ isAuthenticated,
+ authorizeRole(ROLE_GROUPS.ADMIN),
+ positionController.addPosition
+);
// for getting all the position
router.get("/get-all", isAuthenticated, positionController.getAllPositions);
@@ -16,6 +23,7 @@ router.get("/get-all", isAuthenticated, positionController.getAllPositions);
router.post(
"/add-position-holder",
isAuthenticated,
+ authorizeRole(ROLE_GROUPS.ADMIN),
positionController.addPositionHolder
);
diff --git a/backend/services/certificates.service.js b/backend/services/certificates.service.js
index 5a11bdd8..2da773c1 100644
--- a/backend/services/certificates.service.js
+++ b/backend/services/certificates.service.js
@@ -1,4 +1,3 @@
-const puppeteer = require("puppeteer");
const crypto = require("crypto");
const { User } = require("../models/schema");
const renderToPdf = require("../utils/renderPdf");
@@ -7,6 +6,8 @@ const { Certificate } = require("../models/certificateSchema");
const Template = require("../models/templateSchema");
async function generateCertificates(batch) {
+ const puppeteer = (await import("puppeteer")).default;
+
const users = await User.find({
_id: { $in: batch.users },
}).select("personal_info");
@@ -65,6 +66,7 @@ async function generateCertificates(batch) {
})),
};
+
// Generate PDF
const pdfId = await renderToPdf(data, browser);
@@ -120,4 +122,4 @@ async function generateCertificates(batch) {
}
}
-module.exports = generateCertificates;
\ No newline at end of file
+module.exports = generateCertificates;
diff --git a/backend/utils/renderPdf.js b/backend/utils/renderPdf.js
index a4e37810..feb7bbc0 100644
--- a/backend/utils/renderPdf.js
+++ b/backend/utils/renderPdf.js
@@ -1,4 +1,3 @@
-const puppeteer = require("puppeteer");
const handlebars = require("handlebars");
const fs = require("fs");
const path = require("path");
@@ -27,13 +26,20 @@ const watermarkLogoDataUri = loadAsDataUri(
);
async function renderToPdf(data, sharedBrowser = null) {
+ const puppeteer = (await import("puppeteer")).default;
+
let browser = sharedBrowser;
let ownBrowser = false;
let page;
try {
if (!browser) {
- browser = await puppeteer.launch({ headless: true });
+ browser = await puppeteer.launch({
+ headless: true,
+ ...(process.env.PUPPETEER_NO_SANDBOX === "true"
+ ? { args: ["--no-sandbox", "--disable-setuid-sandbox"] }
+ : {}),
+ });
ownBrowser = true;
}
@@ -72,8 +78,6 @@ const watermarkLogoDataUri = loadAsDataUri(
console.log("Certificate successfully generated at", outputPath);
return fileId;
- } catch (err) {
- throw err;
} finally {
if (page) {
try {
@@ -92,4 +96,4 @@ const watermarkLogoDataUri = loadAsDataUri(
}
}
-module.exports = renderToPdf;
\ No newline at end of file
+module.exports = renderToPdf;
diff --git a/docker-compose.yml b/docker-compose.yml
index a6d8d089..f0ecfdfb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -23,6 +23,9 @@ services:
frontend:
build:
context: ./frontend
+ args:
+ # Backend URL as reachable from the user's browser (host port).
+ REACT_APP_BACKEND_URL: http://localhost:8000
container_name: frontend
ports:
- "3000:80" # Map host port 3000 to container port 80 (nginx default)
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index f1ed48c5..5356a5de 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -7,6 +7,10 @@ RUN npm install --legacy-peer-deps
COPY . .
+# Injected at build time: CRA inlines REACT_APP_* into the bundle.
+ARG REACT_APP_BACKEND_URL
+ENV REACT_APP_BACKEND_URL=$REACT_APP_BACKEND_URL
+
RUN npm run build
FROM nginx:stable-alpine
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
deleted file mode 100644
index ccb2ad5e..00000000
--- a/frontend/eslint.config.mjs
+++ /dev/null
@@ -1,28 +0,0 @@
-import react from 'eslint-plugin-react';
-import pkg from 'globals';
-const { browser } = pkg;
-
-export default [
- {
- files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
- plugins: {
- react,
- },
- languageOptions: {
- parserOptions: {
- ecmaFeatures: {
- jsx: true,
- },
- },
- globals: {
- ...browser,
- },
- },
- rules: {
- // ... any rules you want
- 'react/jsx-uses-react': 'error',
- 'react/jsx-uses-vars': 'error',
- },
- // ... others are omitted for brevity
- },
-];
diff --git a/frontend/package.json b/frontend/package.json
index d7728bf7..13c0464f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -37,10 +37,13 @@
"web-vitals": "^2.1.4"
},
"scripts": {
- "tailwind": "tailwindcss",
+ "tailwind": "tailwindcss -i ./src/index.css -o ./src/tailwind.css --minify",
+ "prestart": "npm run tailwind",
"start": "react-scripts start",
+ "prebuild": "npm run tailwind",
"build": "react-scripts build",
"test": "react-scripts test",
+ "lint": "eslint src",
"eject": "react-scripts eject"
},
"lint-staged": {
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 984cbac1..6000d89a 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,8 +1,3 @@
-/* @tailwind base;
-@tailwind components;
-@tailwind utilities; */
-@import "tailwindcss";
-
.App {
text-align: center;
}
diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js
index 1f03afee..a8cf4236 100644
--- a/frontend/src/App.test.js
+++ b/frontend/src/App.test.js
@@ -1,8 +1,22 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
+import { render, screen, waitFor } from "@testing-library/react";
+import App from "./App";
-test('renders learn react link', () => {
- render(