Skip to content

feat(alumni): add verified alumni directory with capacity-guarded mentorship (fixes #283) - #288

Open
MOHITKOURAV01 wants to merge 1 commit into
Sitaram8472:mainfrom
MOHITKOURAV01:feat/issue-283-alumni-mentorship
Open

feat(alumni): add verified alumni directory with capacity-guarded mentorship (fixes #283)#288
MOHITKOURAV01 wants to merge 1 commit into
Sitaram8472:mainfrom
MOHITKOURAV01:feat/issue-283-alumni-mentorship

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Related Issue

Closes #283

Description

Gives the school a record of the people who have left, and current students a way to ask them for guidance. Two things drive the whole design.

1. Nothing reaches a student until staff have verified it

This is the reason the module exists in the form it does. Putting an unverified stranger's contact details in front of a child is the failure mode, so verification is enforced in three places rather than in the UI:

  • The browse query fixes verificationStatus: 'verified' in the filter — it is not derived from a query parameter, so an unverified profile cannot be reached by asking for it.
  • GET /profiles/:id returns 404, not 403, for an unverified profile the caller does not own. A 403 confirms the profile exists, which is itself a small disclosure.
  • verificationStatus, verifiedBy and verifiedAt are absent from the document the create handler constructs. A request carrying verificationStatus: 'verified' has that field dropped rather than honoured.

Editing the identity fields of a verified profile sends it back to the queue. Otherwise "verified" quietly comes to mean "was verified once, possibly describing somebody else now".

2. Contact details are released by an accepted mentorship, not by browsing

An alumni directory complete with everyone's email is how you get alumni to stop registering. Email, phone and LinkedIn are stripped from every response except for:

  • the profile's owner,
  • admin and office staff,
  • a student whose mentorship request that alumnus has accepted.

redactFor(viewer) on the schema is the single place that decides this, in the same style as MeetingSlot.redactFor — so there is one answer rather than one per handler, and adding an endpoint later cannot accidentally leak by forgetting a rule. Other students' requests are stripped for the same reason: a directory showing who else asked for help with what is a directory nobody uses honestly.

The atomic bit

Accepting a request is the only operation here that consumes something finite, so it is the only one that has to be atomic. Both halves of the invariant sit in the filter of one findOneAndUpdate:

{
  _id, 
  mentorships: { $elemMatch: { _id: requestId, status: 'pending' } },
  $expr: { $lt: ['$activeMenteeCount', '$mentorCapacity'] },
}

with the status flip and the counter increment in the same write. An alumnus who offered two places cannot end up with three because three requests were accepted in the same minute — and the person let down there is a volunteer, which is a good way to lose volunteers.

activeMenteeCount exists precisely so the check is expressible in the filter. Counting the array in application code would move the check outside the write, which is the race it is there to prevent.

Completing a mentorship returns the seat, guarded on the request still being accepted so a double-tap cannot decrement the counter twice and leave the mentor looking free when they are not.

Small things I decided deliberately

  • Declining requires a reason. Silence is the worst answer a student can get.
  • Capacity cannot be set below the current mentee count — you cannot un-commit to somebody by editing a number.
  • A graduation year in the future is rejected. That is a typo, not a plan.

Pages / Components Added or Modified

  • backend/models/AlumniProfile.js — profile, verification, mentorship requests, redactFor()
  • backend/controllers/alumniController.js — directory, verification queue, the capacity-guarded accept
  • backend/routes/alumniRoutes.js/api/alumni
  • backend/server.js — route registration (2 lines)
  • frontend/src/pages/Alumni.jsx — directory with filters, request dialog, my-requests
  • frontend/src/components/alumni/AlumniVerificationPanel.jsx — the office's approve/reject queue
  • frontend/src/App.jsx/alumni route (8 lines)

Screenshots

Not attached — the directory is empty without seeded alumni and a shot of an empty state does not tell you much. I can seed a demo set and add screenshots if that would help review.

Checklist

  • npx eslint . passes — 18 problems, the same count as pristine main
  • npx vite build succeeds; Alumni code-splits into its own chunk
  • node --check clean on every backend file
  • Responsive layout verified — the directory is a 3/2/1 column grid
  • Tested against a live database
  • Screenshots attached

Notes for review

The place I would most welcome a second opinion is the redaction rule. I chose "contact details unlock on acceptance" rather than "on request" because a request costs the student nothing, so unlocking on request would let anyone harvest the directory by requesting everybody. If you would rather the office released details manually instead, that is a small change to redactFor and nowhere else — which is the reason it is in one place.

No TeacherDashboard.jsx tab: the verification queue lives inside the /alumni page itself, which keeps this branch off the most contended file in the repo. Merges cleanly against main and against every other open PR, in both orders.

Gives the school a way to keep in touch with people who have left, and
current students a way to ask them for guidance.

The two things this module is actually about:

- Nothing reaches a student until staff have verified it. The browse
  query fixes verificationStatus to 'verified' rather than taking it
  from a parameter, an unverified profile 404s even by direct link, and
  the field is absent from the document the create handler builds so a
  request cannot verify itself.
- Contact details are released by an accepted mentorship, not by
  browsing. redactFor() is the single place that decides this, so there
  is one answer rather than one per handler. A directory that hands out
  everyone's email is a directory alumni stop registering for.

Accepting a request is the only operation that consumes something
finite, so it is the one that is atomic: capacity and the pending check
both live in the filter of one findOneAndUpdate, with the counter
incremented in the same write. An alumnus who offered two places cannot
end up with three because three students were accepted in the same
minute — and the person let down there is a volunteer.

Editing the identity fields of a verified profile sends it back to the
queue, so "verified" cannot come to mean "was verified once, possibly
describing somebody else".

Closes Sitaram8472#283
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Alumni network & mentorship matching with staff verification and capacity-guarded acceptance

1 participant