feat(admin): give the admin dashboard a way in - #130
Merged
udaycodespace merged 1 commit intoAug 30, 2026
Conversation
There was no way to sign in as an admin. $ grep -rn "api/admin/login" frontend/src $ POST /api/admin/login is the only issuer of a token carrying role: "admin", and nothing in the browser called it — no route, no form, no link. The normal sign-in is not a way in either: validateRegistration refuses a self-assigned role, which is the point of udaycodespace#55, so no registered account can hold it, and there is no seeding script. Both ends were complete and neither could reach the other: a constant-time credential check and eight guarded routes on one side, AdminHome, PaymentRecords, ActivityLogs and the admin course table on the other. Calling the endpoint by hand did not help. readSession needs both a valid token and a stored user with an id, and the response carried only a token, so there was nothing to write under the `user` key and isAuthenticated stayed false. - utils/adminAccount.js builds the admin identity once. authMiddleware had it inline; the login response now returns the same object under userData, the key /api/user/login already uses. No email on it — the admin is a credential pair in the environment, not an account with a mailbox. - /admin/login, behind PublicOnlyRoute like /login, with a link in the footer under Legal. It writes the session through the same writeSession the learner sign-in uses and refreshes the auth context before navigating, or ProtectedRoute would bounce the redirect straight back. - lib/adminSession.js refuses a token with no account and refuses a non-admin role at the form, where there is still somewhere to show a message. A 500 from an unconfigured server passes through verbatim. The credential check, the token and every admin route and screen are unchanged. They did not need editing; they needed reaching. The new backend tests went into admin-auth.test.js rather than a new file: sixteen suites already start their own MongoMemoryServer in parallel, and a seventeenth tips the run into startup timeouts across unrelated suites. backend 517 pass (505 before), frontend 229 pass (214 before). Closes udaycodespace#125
4 tasks
udaycodespace
self-requested a review
August 30, 2026 06:20
Owner
|
Reviewed this, @MOHITKOURAV01 . This cleanly connects the existing admin authentication backend with the frontend without changing the underlying admin credential or authorization flow. The shared admin account construction, session handling, All backend and frontend tests are passing, and the frontend build is clean. Approved and merging. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #125.
The defect
There was no way to sign in as an admin.
POST /api/admin/loginis the only issuer of a token carryingrole: "admin",and nothing in the browser called it.
App.jsxdeclared eight routes and noneof them was
/admin;Login.jsxposts to/api/user/loginand nothing else.The normal sign-in is not a way in either:
validateRegistrationrefuses aself-assigned role — the point of #55 — so no account created through
/registercan holdtype: "admin", andbackend/scripts/containsdedupeUserEmails.jsand nothing else.Both ends were complete and neither could reach the other:
adminLoginController, constant-time check, activity log entry[authMiddleware, checkRole(["admin"])]AdminHome,PaymentRecords,ActivityLogs, adminAllCoursesCalling the endpoint by hand did not help
Suppose an operator ran the request in a console and wrote the token into
storage. The session layer still refused it:
readSessionneeds both a valid token and a stored user with an id. Theendpoint returned neither:
So
isAuthenticatedstayed false,ProtectedRoutebounced back to/login,and
getUserRole(undefined)returned''— which would have rendered "Thisaccount has no dashboard yet" even if the guard had let it through.
What is here
utils/adminAccount.js. The admin is not ausersrow;authMiddlewarerecognises the reserved id without a lookup and built the identity inline. That
literal now comes from
buildAdminAccount, and so does the account in the loginresponse, so the two cannot describe the admin differently. It carries
_idforparseStoredUser,typeandroleforgetUserRole, andnamefromADMIN_USERNAMEso the navbar says which operator account is signed in. Itcarries no email — the admin is a credential pair in the environment, not an
account with a mailbox, and a fabricated address would be worse than an absent
one.
The login response carries that account under
userData, the key/api/user/loginalready uses./admin/login, inPublicOnlyRoutelike/login, with a link in thefooter under Legal — it is for the operator account configured on the server,
not for learners, but it has to exist somewhere and until now it existed
nowhere. It writes the session with the same
writeSessionthe learner sign-inuses and calls
refresh()on the auth context before navigating:AuthProviderreads storage on mount and on the
storageevent, which only fires in othertabs, so without that the redirect would arrive before the provider knew there
was a session and
ProtectedRoutewould bounce it straight back.lib/adminSession.jsrefuses a response carrying a token with no account —the exact shape the endpoint used to return — and refuses a non-admin role at
the form, where there is still somewhere to show a message, rather than at the
dashboard where the only outcome is a blank panel. A 500 from an unconfigured
server ("Admin access is not configured on this server") passes through
verbatim, because that sentence tells an operator to set the environment
variables instead of retyping a password that can never work.
Tests
backend/tests/admin-account.test.js— 7 tests on the account object, nodatabase. What it carries, what it deliberately does not, and that a fresh
object is returned each call —
authMiddleware's other branch assigns toreq.user, so a shared singleton would be a cross-request mutation.backend/tests/admin-auth.test.js— 5 tests added. The account satisfies thebrowser's own
parseStoredUserrule, restated on this side so the two cannotdrift, and survives the JSON round trip
localStorageputs it through; thelogin response and the account
authMiddlewarebuilds are compared field byfield through a probe route; a rejected sign-in carries no account; and the body
is checked for the configured password and for anything shaped like a bcrypt
hash.
These went into the existing file rather than a new one on purpose: sixteen test
files each start their own
MongoMemoryServerandnode --testruns them inparallel, and a seventeenth tipped this machine into
Instance failed to start within 10000msacross unrelated suites.frontend/src/lib/adminSession.test.js— 15 tests, including the defect itself:{ success: true, token }with no account is refused.docs/issue-125-admin-signin.mdhas the write-up.Checklist
cd backend && npm test— 517 pass (505 onmain, 12 added)cd frontend && npm test— 229 pass (214 onmain, 15 added)cd frontend && npm run buildnpm run lint— does not pass onmain(69 problems) and does not here.SiteFooter.jsxreports the same 1 before and after, a pre-existingunused
Reactimport.lib/adminSession.js,AdminLogin.jsxandApp.jsxlint clean.Notes
The credential check, its constant-time comparison, the production refusal of a
plaintext
ADMIN_PASSWORD, the activity log entries, the token and every adminroute and screen are unchanged. They did not need editing; they needed reaching.