GradeX platform improvement plan organized by category and priority

Critical / security
High priority
Medium
Nice to have
Critical
Reset tokens use Math.random() — not cryptographically safe
In authController.js, reset tokens are built with two Math.random().toString(36) calls. This is predictable and brute-forceable.
Replace with Node's built-in crypto:
import { randomBytes } from "crypto";
const token = randomBytes(32).toString("hex");
This gives 256 bits of entropy vs the current ~70 bits.
Critical
No rate limiting on password reset endpoint
The POST /api/auth/reset-password route has no rate limiter applied. An attacker can trial millions of short tokens quickly. Apply the authLimiter already defined in server.js.
Critical
Auto-created admin in server.js with a hardcoded password
The createAdminIfMissing() function creates admin01 with password Admin@123 on every cold start if no admin exists. In production anyone who sees the code can log in before the real admin changes the password.
Remove this entirely and rely on the POST /api/auth/setup/admin endpoint (which is already guarded by ADMIN_SETUP_KEY) for first-run setup. If you keep it, at minimum read the password from an env variable: process.env.DEFAULT_ADMIN_PASSWORD, refuse to start if it is missing, and force requiresPasswordChange: true.
High
Support inbox returns all messages regardless of role
In supportController.js → getMessages, the query for non-admin users filters by studentId: req.user.id — but this is the User _id, while SupportMessage.studentId stores the User _id too (no profile lookup). This works for students but any teacher hits the same route and currently sees nothing because their userId never matches a studentId. More importantly, the frontend calls /support/all without role awareness, meaning the filter logic is only on the server. Tighten this.
High
No Helmet CSP header configured
Helmet is used but contentSecurityPolicy is not configured — it defaults to a broad policy. Add an explicit CSP to prevent XSS escalation, especially since the app renders arbitrary student names from the database into the DOM.
Medium
JWT carries role in payload — not re-validated against DB on every request
The protect middleware looks up the user from the DB on every request, which is good. But the authorize middleware uses req.user.role from the DB record, so a role change is reflected immediately — this part is fine. The issue is that user.status check happens but user.role changes are not propagated to existing tokens (no role version). Consider adding a roleVersion alongside tokenVersion.
Bug
Settings page calls showNotification but never imports it
In Settings.jsx, showNotification("تم تحديث الإعدادات بنجاح", "success") is called inside handleUpdate and handleLogoUpload but the hook is never destructured. This throws a runtime error silently on save.
const { showNotification } = useNotification();
Add this line at the top of the component alongside const { refreshSettings } = useAuth().
Bug
Logout redirects to /login which doesn't exist
In AuthContext.jsx → logout(), the redirect is window.location.href = "/login". Your router has no /login route — the login page is at /. This results in the wildcard route catching the redirect and bouncing to / anyway, but only after a flash. Change to window.location.href = "/".
High
useUploadFlow.js hook is written but never used
A well-structured two-step upload hook exists at hooks/useUploadFlow.js but AdminDashboard.jsx reimplements the same logic inline with its own state. The hook also uses a different API base path (/admin/upload-preview without /api) that would fail. Either use the hook or delete it — the duplication will cause divergence.
High
confirmUpload doesn't pass subjectName in the hook
In useUploadFlow.js → confirmUpload, the POST body uses summary.grade and summary.department but the backend's confirmUpload controller reads req.body.subjectName and req.body.isPublished — neither of which the hook sends. The hook is broken by design.
High
Student rank calculation runs a separate count query per student (N+1)
In adminUploadController.js → getStudents fallback, Student.countDocuments is called inside a Promise.all(paginated.map(...)). For 50 students per page that's 50 extra DB queries. The aggregation path avoids this with $setWindowFields, but the fallback is used in older MongoDB.
Medium
Certificate component receives wrong data shape
In StudentDashboard.jsx, <Certificate student={data} /> is passed but Certificate.jsx maps subject.grade and subject.details['درجة الفصل الأول']. The actual student data uses subject.currentScore and subject.details as a Map. The certificate will render empty grade cells for every student.
High
bulkCreateStudents processes students one-by-one in a for-loop
In adminUploadController.js → bulkCreateStudents, each student triggers two sequential await calls (User.create + Student.create) inside a serial loop. For 200 students this means ~400 sequential DB round-trips. Use insertMany in batches or at minimum Promise.all with concurrency limiting.
High
No database indexes on frequently queried fields
Fields like Student.grade, Student.department, Student.section, and Student.isPublished have no compound index but are used together in every admin filter query. Add a compound index: { grade: 1, department: 1, section: 1, average: -1 }.
Medium
Notification EventEmitter leaks on large deployments
The notificationEvents EventEmitter in utils/events.js has no setMaxListeners call. With many concurrent SSE connections it will emit Node.js memory leak warnings. Set notificationEvents.setMaxListeners(0) or use a proper pub/sub adapter for multi-process deployments.
Medium
Admin dashboard fetches stats and options independently on every mount
The 60-second in-memory cache in AdminDashboard.jsx is per-component instance and is lost on unmount. Use React Query, SWR, or a global Zustand/Context store so navigating away and back doesn't re-fetch. This also applies to the options dropdown which is fetched on 4 different pages independently.
Low
Images are served via express.static with no cache headers
Uploaded avatars and logos are served with default no-cache headers. Add express.static(uploadsDir, { maxAge: '7d' }) since filenames are timestamp-based and change on each upload.
High
Destructive actions use window.confirm() — replace with custom modals
Delete student, bulk publish, and delete subject all use the browser's native window.confirm() dialog. This breaks your polished RTL design, can't be styled, and is blocked in some embedded contexts. Create a reusable <ConfirmModal> component consistent with your existing modal style.
High
Mobile layout: main content overlaps sidebar on scroll
The sidebar uses fixed right-0 with a z-40 backdrop, but on mobile the main content has pt-20 assuming only the top navbar, not accounting for the sidebar trigger state. Test the ManageStudents and AdminDashboard pages on a 375px viewport — the table overflows horizontally and the sticky header overlaps content.
High
Empty states are inconsistent across pages
Some pages show a nice illustrated empty state (StudentDashboard), others show raw italic text inside a table cell, and TeacherDashboard shows nothing at all when stats is empty. Standardize with a shared <EmptyState icon="..." title="..." message="..." /> component.
Medium
Notification panel closes on any outside click but not on ESC key
The notifications dropdown in Navbar.jsx toggles on button click but has no useEffect listener for outside clicks or the Escape key. Users expect to close any open popover with Escape. Also, clicking inside the notification panel should not close it.
Medium
Upload flow gives no progress indication for large files
The upload triggers a full-page spinner overlay with no progress percentage. For a 10MB Excel file on a slow connection this looks like a hang. Use Axios's onUploadProgress callback to show a real progress bar inside the drop zone.
Medium
Teacher dashboard shows no feedback when file has parse errors
The preview response includes an errors array from the parser (e.g. "Could not find Grade column"). The TeacherDashboard discards this array completely — errors are never shown to the teacher. Display them as a dismissible warning panel below the upload form.
Low
Dark mode preference is not synced across tabs
The theme toggle in Navbar writes to localStorage and sets a DOM attribute, but uses a local useState. Opening the app in a second tab won't inherit the theme until a manual toggle. Use a storage event listener to sync the preference across tabs.
High
adminUploadController.js is 550+ lines — split it into domain controllers
This single file handles students CRUD, teachers CRUD, bulk operations, analytics, settings, logo upload, and notifications. Extract into: studentController.js, teacherController.js, settingsController.js, analyticsController.js. Routes stay the same — just import from different files.
High
Dropdown options fetched on 4+ separate pages with no shared cache
AdminDashboard, ManageStudents, TeacherDashboard, EditStudentModal, and TeacherApproval all independently call GET /admin/options. Lift this into a React Context (e.g. OptionsContext) fetched once at app init and reused everywhere.
Medium
getSettings() creates a Settings document if missing — unexpected side effect
The helper in adminUploadController.js silently creates a Settings record as a side effect of a GET request. This violates the principle of least astonishment and can create orphaned documents. Move the creation to application startup (alongside createAdminIfMissing) and throw in getSettings if none is found.
Medium
Inline prompt() used in Settings.jsx for adding list items
The addItem function calls window.prompt() — a modal dialog that blocks the tab and can't be styled. In RTL it renders LTR, which looks broken. Replace with an inline input + add button pattern within the settings card.
Medium
No global API error handling beyond 401 redirect
The Axios interceptor in api/axios.js only handles 401. Errors like 403 (forbidden), 429 (rate limited), or 500 (server crash) reach components as raw Axios errors with no shared handling. Add a response interceptor that shows a showNotification toast for common error codes before re-throwing.
Low
No environment validation at startup
Only JWT_SECRET length is validated. Missing MONGO_URI, ADMIN_SETUP_KEY, or PORT fail silently or produce cryptic errors. Add a startup check that lists all required env vars and exits with a clear message if any are absent.
High
Audit log: record who changed what and when
There is no record of which admin or teacher uploaded grades, approved a teacher, or changed a student's score. Add an AuditLog model with fields: actor (userId), action, target, diff, timestamp. Display it in an admin-only Activity tab.
High
Bulk publish confirmation preview before applying
The bulk publish button in ManageStudents applies immediately to potentially hundreds of students. Add a preview step: "You are about to publish results for 143 students in Grade 3 / Electrical Engineering. Continue?" — with a count of currently unpublished students affected.
Medium
Student score history / rounds tracking
The data model tracks previousScore vs currentScore for one round only. Add a history: [{ round, score, uploadedAt }] array per subject so students can see how they progressed across all rounds, and teachers can see trend lines.
Medium
Excel template download for correct upload format
Teachers frequently upload wrongly formatted files, causing the parser to fail. Provide a "Download template" button that generates a correctly structured Excel file pre-filled with the expected column headers so teachers know exactly what format to use.
Medium
Per-student individual result sharing via secure link
Admins currently must share credentials with every student. Add a "Share result" feature that generates a time-limited public link (no login required) for a specific student's published result — useful for parents or external institutions.
Low
Class-wide analytics export (PDF report per grade/department)
Teachers and admins can see analytics on screen but cannot export them. Add a server-side PDF report generator (using pdfkit or puppeteer) that produces a formatted grade summary per class, including pass rate, top performers, and subject averages.