More Projects
59 in total
CCS Examination Portal
Contributor — face-verification service · Creative Computing Society · 2023
Online examination platform built for society recruitment, where the interesting requirement is not delivering questions but stopping people cheating. Tab-switch detection with a three-strike audit trail — detected, not prevented — a webcam still captured every sixty seconds, and a per-candidate randomised draw from a bank of hundreds of questions, on a React and Express stack over MongoDB. The substantial piece written for this cycle is not in that repo at all: face_rec, a separate Django REST service that verifies a candidate's face against their registered photo, which held 936 encodings for the sitting it served. The portal ran for close to 2000 candidates across the recruitment.
~2,000 candidates · 936 encodings enrolled
Project Details
Contributor — face-verification service
Creative Computing Society
2023
~2,000 candidates · 936 encodings enrolled
Society · Web
The society recruits its first-year intake by examination, and the candidates sit it on their own machines, in their own rooms, with nobody standing behind them. That is the whole design problem. An exam portal that only has to deliver questions and collect answers is a weekend project; one that has to make an unproctored result defensible is not.
The source is creative-computing-society/quiz-portal, a MERN application with 276 commits from
August 2022 to October 2023 across six contributors, package name
ccs-recruitment-portal-mern-stack. It is a platform the society maintained across recruitment
cycles rather than a one-off build. His nine commits all fall in the 2-4 October 2023 window and
are operational rather than architectural: "Updated Image Upload and mailings", "Timing and Mails
updated", "Application Number changed to Roll Number", "Updated config.js url", "Camera Mirror
added". The substantial thing he wrote for this cycle is not in this repo at all. It is
face_rec, his own face-verification service, and it gets its own section below.
How a paper is drawn
The anti-cheat that matters is not surveillance, it is that no two candidates are looking at the
same paper. questionController.getQuestions runs three queries in parallel for easy, medium and
hard questions filtered to the candidate's slot, shuffles each result with a Fisher-Yates pass,
and takes four easy, three medium and three hard. Ten questions, drawn per candidate, out of
whatever the bank holds for that sitting. The society's own report for the cycle puts the bank in
the hundreds, and the code is agnostic about its size.
The important part is what happens next. The drawn set is written back onto the user document as
newAssignedQuestions with beenAssigned set true, and every subsequent request returns the
stored array rather than redrawing. That is the difference between a working exam and a broken
one. If the paper were recomputed on each request, a refresh or a dropped connection would hand
the candidate a different exam and the answers already submitted would be attached to questions
nobody is being asked any more. The draw is a one-time event whose result is state.
Scoring is on the server and on a one-shot guard. answerController.checkAnswers refuses to run
at all if hasAttempted is already true, walks the submitted [questionId, answer] pairs,
awards the question's virtual points (two for easy, three for medium, four for hard), subtracts
one for a wrong answer and zero for unanswered, and stores a per-question record of what was
given against what was correct. Then it overwrites the JWT cookie with a ten-second expiry, which
logs the candidate out at the moment of submission.
Randomised selection is a better lever than watching people, because it removes the incentive rather than trying to catch the act. Two candidates in the same room with the same paper, comparing answers out loud, look perfectly attentive on camera. Two candidates with different papers have nothing to compare. It also changes what a leak costs: with one paper, a question escaping compromises the whole sitting, and with a per-candidate draw it compromises the fraction who happened to receive it.
Two cameras and a three-strike counter
The browser-side defences are narrower than a description of them usually implies, and worth being precise about.
Focus loss is detected, not prevented. LeftAside.js binds $(window).blur, checks
document.hasFocus(), and increments a local counter, escalating the on-screen warning each
time. On the third, it PATCHes /users/cheatAttempt, force-submits the test, calls
document.exitFullscreen() and routes to /disqualified. The server keeps its own
cheatAttempts count and the admin leaderboard filters to candidates with two or fewer. A web
page can notice it lost focus and it can react; it cannot stop the operating system putting
another window on top, and it cannot see a second laptop next to the first one. This is a
deterrent with an audit trail, not an enforcement mechanism, and it was correctly not the primary
control.
The webcam runs at both ends of the process. At sign-up, SignUp/Components/webcam.js mounts
react-webcam at 400x400 with facingMode: "user", enumerates video devices and offers a
switch-camera button only when more than one exists, and supports retake before the shot is
committed. The captured data URL goes to the backend, where imageController.imageDecode strips
the prefix, base64-decodes it and writes ./images/<name>_<applicationNumber>.jpg. That is the
reference face.
During the test, RightAside.js keeps a react-webcam mounted beside the countdown and fires
capturePicture() on a 60-second interval, posting the stripped base64 and the candidate's roll
number to /users/save. A commented-out toast in that handler reads "Warning:Face Recognition
Failed to verify You!". That line is the seam where the next section plugs in.
face_rec, the identity check behind it
akarsh911/face_rec is a Django REST service, 17 commits, all his, all dated 10 October 2023,
which is the recruitment exam window. It ran on the society's own host: the encoding path is
hardcoded as /home/ccs/face_rec/api/encodings/, which is not a path that exists on a laptop.
It exposes exactly one endpoint. POST /api/verify/ takes {roll: int, image: base64}, decodes
the frame with numpy and OpenCV, runs face_recognition.face_locations followed by
face_encodings, and compares the result against one stored encoding: the .pkl belonging to
that roll number and no other. It is a 1:1 verification rather than a 1:N search across the
cohort, which is the right call. Asking "is this the person who signed up under roll 102030390"
is a cheap, bounded comparison; asking "who is this out of everyone" is expensive, gets slower as
the cohort grows, and produces false matches at exactly the moment you least want them.
api/encodings/ holds 936 .pkl files, one per roll number. That is the size of the enrolled
cohort it was serving.
The failure handling is the part that makes it usable as evidence rather than as a vibe. A roll
number with no encoding on disk is appended to failed_result.json with a timestamp, a status
string and the exact path that was tried. A face that does not match is appended with a status of
"Image Not Matched", and the offending frame is written to false_results/<roll>.jpg. So a
disputed disqualification has a file with a time on it and a picture attached, rather than a
counter that says three.
Two copies of the service sit in the tree and the difference between them is the deployment
history. The outer api/ is the production version with POSIX absolute paths and base64 input.
The nested face_rec/api/ is the local Windows original, reading encodings\{roll}.pkl and
taking the image as a file upload with a save_image helper. The move from one to the other is
the move from a laptop to the society's server.
This is a different system from the later face-gallery work, which is December 2024 on FastAPI,
DeepFace and MongoDB. face_rec is a year earlier and purpose-built for this exam.
Surface area
- Per-candidate draw of four easy, three medium and three hard questions from the sitting's bank, persisted on the user document so the paper is stable across refreshes.
- Server-side scoring with difficulty-weighted points, negative marking, and a
hasAttemptedguard that makes submission idempotent. - Focus-loss detection with escalating warnings and a three-strike auto-submit to a disqualification route.
- Reference face capture at sign-up with device enumeration, camera switching and retake.
- Periodic face capture during the attempt, posted once a minute against the candidate's roll number.
- 1:1 face verification service in Django REST over
face_recognitionand OpenCV, holding 936 per-roll encodings, with auditable failure logging and saved mismatch frames. - Spreadsheet question loading through
upload_ques.js, readingques.xlsxinto Mongo. - Two-shift scheduling, with candidates alternated between slots at sign-up and questions partitioned by slot so the two sittings never see the same items.
- OTP-as-password sign-up, generating a four-digit code, mailing it with the candidate's shift time, and using it as the account password.
- Interview slot layer after the quiz through
interviews.xlsxanddelete_slot1.js. - WhatsApp broadcast via a Puppeteer script driving
web.whatsapp.comclick-to-chat links. - Bulk mail through Nodemailer with hand-built Outlook-namespaced XHTML templates.
- Express hardening with helmet,
express-mongo-sanitize,xss-clean,validatorand a 100kb body limit, behind JWT auth with an admin-only middleware.
Operations were half the system
The parts that look least like engineering are the ones a recruitment cycle actually runs on.
whatsappBot.js launches Puppeteer non-headless, waits for a human to scan the WhatsApp Web QR,
then walks a list of students opening click-to-chat URLs, typing into the contenteditable and
pressing Enter, with twenty-second sleeps between steps because there is no reliable ready
signal. It is a scraper wearing a bot's clothes and it is honest about it. bulk_emails.js sends
the designed HTML mails. The exam's own timing lives in the front end's config.js as
slot1_time, slot2_time and a test_duration of 40 minutes.
Honest limitations
The old description of this portal called the exam definition configurable by administrators. It is not. There is no exam model, no settings collection and no admin screen for any of it: the duration and the two start times are constants in a front-end config file, and changing them is a rebuild and a redeploy. The admin surface that does exist is three endpoints, for creating a question, listing all questions, and reading the leaderboard.
express-rate-limit is in the dependency list and the limiter is commented out in app.js, so
the login endpoint took the burst unthrottled. The countdown is a client-side react-countdown
seeded from a timePast value in localStorage, which means the clock is editable by the
candidate. upload_ques.js reads row.option1 and row.option2 but row.options3 and
row.options4, so two of every four options load as undefined unless the spreadsheet carries the
mismatched headers. A live Gmail app password is hardcoded in bulk_emails.js, and the
images/ directory still holds his own test captures from building it.
face_rec has its own list. DEBUG is True, CORS_ALLOW_ALL_ORIGINS is True and the Django
secret key is committed. The deployed copy on disk contains base64..b64decode(image) with two
dots, which is a syntax error, and the success path writes images.json using a
date_time_string that is only assigned on the failure branch. So the snapshot that survived is
not a running one, whatever ran on the night. And the whole verification is a single-encoding
comparison at the library's default tolerance, which is a reasonable filter for "is somebody
else's face in this frame" and not a reasonable basis for disqualifying anyone on its own.
The report figure for the cycle is close to 2,000 candidates across the recruitment; the 936
encodings are what face_rec held for the sitting it served. Both are worth having on the page
because they measure different things.
Project Details
Contributor — face-verification service
Creative Computing Society
2023
~2,000 candidates · 936 encodings enrolled
Society · Web