authController.js, reset tokens are built with two Math.random().toString(36) calls. This is predictable and brute-forceable.crypto:
import { randomBytes } from "crypto";
const token = randomBytes(32).toString("hex");
This gives 256 bits of entropy vs the current ~70 bits.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.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.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.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.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.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.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().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 = "/".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.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.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.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.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.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 }.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.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.express.static(uploadsDir, { maxAge: '7d' }) since filenames are timestamp-based and change on each upload.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.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.<EmptyState icon="..." title="..." message="..." /> component.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.onUploadProgress callback to show a real progress bar inside the drop zone.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.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.studentController.js, teacherController.js, settingsController.js, analyticsController.js. Routes stay the same — just import from different files.GET /admin/options. Lift this into a React Context (e.g. OptionsContext) fetched once at app init and reused everywhere.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.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.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.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.AuditLog model with fields: actor (userId), action, target, diff, timestamp. Display it in an admin-only Activity tab.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.pdfkit or puppeteer) that produces a formatted grade summary per class, including pass rate, top performers, and subject averages.