More Projects
59 in totalSRCC Society Website
Contributor · 2024
Society website with dual-provider sign-in — Google OAuth for everyone and Azure MSAL for institutional accounts, two doors onto one JWT and no password anywhere — over an Express/Mongoose backend that is honestly a scaffold: the verify and admin guards are written and commented out, so every event endpoint still answers unauthenticated. The Vite React client is a single sign-up route using jotai for state.
Project Details
Contributor
2024
Society · Web
A college fest has two populations that look the same in a database and behave nothing alike. The college's own students already have institutional accounts and a roll number; students from everywhere else have neither, need different fields collected, and are the ones a pass system actually has to police. Running both through one form and one login is where these sites usually get messy.
The messy version is one users collection with every field nullable and a type string, and it
degrades in a predictable way. Half the fields are meaningless for half the rows, uniqueness on the
roll number cannot be enforced because outside students do not have one, and every handler grows a
branch on the type flag anyway. Splitting the two populations at the schema level costs you a second
lookup on every authenticated request and buys back the ability to make each collection say what it
actually means.
Two doors, two records
Sign-in runs through two identity providers. Institutional accounts come in over Azure MSAL
(@azure/msal-browser with the React bindings, pointed at a specific tenant authority and caching in
sessionStorage); everyone else authenticates with Google through @react-oauth/google. The data
layer mirrors that split with two Mongoose collections instead of one polymorphic user:
SrccStudent carries roll number, year of study and course, while NonSrccStudent carries state,
city, college name, year and stream. Both share the common block - full name, date of birth, email,
phone, gender, referral code, Instagram and LinkedIn - and only SrccStudent carries the isAdmin
flag, because an outside student is never an organiser. Registration lowercases the email before the
uniqueness check, and both paths issue the same JWT.
Auth middleware resolves a token by trying the first collection, then the second, and tags the
request with which one matched. Every downstream handler reads req.userType rather than
re-querying.
Feature set
- JWT in an httpOnly cookie, 30-day expiry,
sameSite: strict,secureoff only in development, issued by a single helper both registration paths call. - Event CRUD for admins covering name, date, start and entry times, posters, location, bullet-point details and an open/closed registration flag.
- Pass accounting on the event itself. Editing an event's pass count adjusts the available count by the delta rather than overwriting it, so passes already handed out are not lost.
- Separate registration counters for in-college and out-of-college students, plus a footfall field, so an organiser can see the mix before deciding who to admit.
- A waitlist by default. Registrations land as
Waitlistand an admin moves them toAcceptedorRejected, singly or through a bulk endpoint that walks a list of ids. - Gate scanning that refuses twice. Marking entry rejects a pass that was never accepted, and rejects one whose entry is already recorded, so a screenshotted pass cannot walk in a second person.
- Self-service unregistration that reverses the accounting it caused: the correct population counter goes down, and an already-accepted pass is returned to the available pool rather than quietly lost.
- A registrations view that resolves each row's user from whichever collection its
userTypenames, so an organiser sees names and colleges rather than object ids. - Mail as a queue, not a function call. Every message becomes a document with recipient, subject, template code, parameters, status, retry count and a priority integer.
- A spooler that drains it by priority, waking every five seconds, resolving template codes
to render functions, pacing two seconds between sends, incrementing
retryCounton failure and closing the document out after three attempts. - A centralised error handler that rewrites Mongoose
CastErroron an ObjectId into a clean 404 and strips stack traces in production. - Public, authenticated and admin route tiers on the event router, with an admin guard that
checks the
isAdminflag on the resolved user.
Why the mail goes through a queue
For a normal web app, sending mail inline is fine. For a fest it is not, and the reason is the shape of the load: nothing happens for a week, then an admin accepts four hundred registrations in one click and four hundred messages need to leave. Sending them from inside that request means the organiser watches a spinner for several minutes and a single SMTP hiccup takes the whole batch down with no record of how far it got.
Making the message a document inverts that. The accept handler writes rows and returns; a separate
loop drains them. Because the row carries its own state, a failed send is a retryCount increment
rather than a lost email, and because it carries a priority integer, a time-critical announcement
sorts ahead of a backlog of welcome mails already in the queue. The two-second pacing between sends
is there for the same reason the queue is: Gmail's transport, which is what the nodemailer transporter
is configured against, will throttle a burst that arrives all at once.
Where it was left
This was a contributor role on a society project, and the backend is honestly a scaffold rather than a finished service. Worth saying plainly, because the gap between what is designed and what is wired up is large in places.
The route tiers are the clearest example. The event router is laid out in three blocks - public,
logged-in, admin - with the verify and admin-guard use calls written and then commented out, so
as it stands every event endpoint including the admin ones answers unauthenticated. The admin guard
itself is written but never exported. The controllers reference an Event model and a
sanitizeInput helper that are not imported into those files. The registration writer names its
fields event and user while the schema declares eventId and userId, and markEntry writes
the string Done into a field whose enum offers Pending and Entered. The mail spooler's
recursive checkAndUpdate is never called from the server entrypoint, and its success branch has the
save commented out, so a message that sent would never be marked sent.
The login handlers are the other unfinished piece: both accept an id token from the client and check that the fields are present rather than verifying the signature against the provider, which is the step that turns a provider login into a trustworthy one. The client side is a single sign-up route that runs both providers and branches to the right registration form, with the rest of the site not started.
Vite and React on the client, with jotai pulled in for shared state and jwt-decode reading the
Google credential's claims straight in the sign-up component. Express and Mongoose on the server,
deployed to Vercel. Contributor on the project.
Project Details
Contributor
2024
Society · Web