Skip to content

feat(admin): give the admin dashboard a way in - #130

Merged
udaycodespace merged 1 commit into
udaycodespace:mainfrom
MOHITKOURAV01:fix/125-admin-signin
Aug 30, 2026
Merged

feat(admin): give the admin dashboard a way in#130
udaycodespace merged 1 commit into
udaycodespace:mainfrom
MOHITKOURAV01:fix/125-admin-signin

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Closes #125.

The defect

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. App.jsx declared eight routes and none
of them was /admin; Login.jsx posts to /api/user/login and nothing else.

The normal sign-in is not a way in either: validateRegistration refuses a
self-assigned role — the point of #55 — so no account created through
/register can hold type: "admin", and backend/scripts/ contains
dedupeUserEmails.js and nothing else.

Both ends were complete and neither could reach the other:

built unreachable
adminLoginController, constant-time check, activity log entry no caller
eight routes behind [authMiddleware, checkRole(["admin"])] no token
AdminHome, PaymentRecords, ActivityLogs, admin AllCourses never rendered

Calling 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:

return parsed._id || parsed.id ? parsed : null;      // parseStoredUser
...
if (!user || !isTokenValid(token, nowMs)) {          // readSession
  return { isAuthenticated: false, user: null, token: null, role: '' };
}

readSession needs both a valid token and a stored user with an id. The
endpoint returned neither:

return res.status(200).send({ success: true, token, message: "Admin login successful" });

So isAuthenticated stayed false, ProtectedRoute bounced back to /login,
and getUserRole(undefined) returned '' — which would have rendered "This
account has no dashboard yet" even if the guard had let it through.

What is here

utils/adminAccount.js. The admin is not a users row; authMiddleware
recognises the reserved id without a lookup and built the identity inline. That
literal now comes from buildAdminAccount, and so does the account in the login
response, so the two cannot describe the admin differently. It carries _id for
parseStoredUser, type and role for getUserRole, and name from
ADMIN_USERNAME so the navbar says which operator account is signed in. It
carries 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/login already uses.

/admin/login, in PublicOnlyRoute like /login, with a link in the
footer 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 writeSession the learner sign-in
uses and calls refresh() on the auth context before navigating: AuthProvider
reads storage on mount and on the storage event, which only fires in other
tabs, so without that the redirect would arrive before the provider knew there
was a session and ProtectedRoute would bounce it straight back.

lib/adminSession.js refuses 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, no
database. What it carries, what it deliberately does not, and that a fresh
object is returned each call — authMiddleware's other branch assigns to
req.user, so a shared singleton would be a cross-request mutation.

backend/tests/admin-auth.test.js — 5 tests added. The account satisfies the
browser's own parseStoredUser rule, restated on this side so the two cannot
drift, and survives the JSON round trip localStorage puts it through; the
login response and the account authMiddleware builds are compared field by
field 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 MongoMemoryServer and node --test runs them in
parallel, and a seventeenth tipped this machine into Instance failed to start within 10000ms across 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.md has the write-up.

Checklist

  • cd backend && npm test — 517 pass (505 on main, 12 added)
  • cd frontend && npm test — 229 pass (214 on main, 15 added)
  • cd frontend && npm run build
  • npm run lint — does not pass on main (69 problems) and does not here.
    SiteFooter.jsx reports the same 1 before and after, a pre-existing
    unused React import. lib/adminSession.js, AdminLogin.jsx and
    App.jsx lint 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 admin
route and screen are unchanged. They did not need editing; they needed reaching.

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
@udaycodespace
udaycodespace self-requested a review August 30, 2026 06:20
@udaycodespace udaycodespace added ECSoC26 Required label for a PR to be eligible for Sentinel scoring good-pr PA-awarded bonus for an exceptionally executed PR — +15 XP good-backend PA-awarded bonus for outstanding backend work — +50 XP labels Aug 30, 2026
@udaycodespace

Copy link
Copy Markdown
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, /admin/login route, role validation, and auth-context refresh are all addressed properly. The tests also cover the important failure case where a token is returned without the account data.

All backend and frontend tests are passing, and the frontend build is clean.

Approved and merging.

@udaycodespace
udaycodespace merged commit 408341a into udaycodespace:main Aug 30, 2026
1 check passed
@ecsoc-sentinel ecsoc-sentinel Bot added the ECSoC26-L3 Difficult, auto-assigned by Sentinel — 15 points label Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26-L3 Difficult, auto-assigned by Sentinel — 15 points ECSoC26 Required label for a PR to be eligible for Sentinel scoring good-backend PA-awarded bonus for outstanding backend work — +50 XP good-pr PA-awarded bonus for an exceptionally executed PR — +15 XP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: The admin dashboard cannot be signed in to — there is no admin login screen, and /api/admin/login returns a token with no account beside it

2 participants