CCS Examination Portal
Completed
Society · Web

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.

SCALE

~2,000 candidates · 936 encodings enrolled

Built with
ReactReact
Node.jsNode.js
ExpressExpress
MongoDBMongoDB
PythonPython
Django REST FrameworkDjango REST Framework
face_recognition
OpenCV
Project Details

STATUS
Completed
ROLE

Contributor — face-verification service

ORGANISATION

Creative Computing Society

YEAR

2023

SCALE

~2,000 candidates · 936 encodings enrolled

TYPE

Society · Web

TAGS
Proctoring
Assessment
Anti-cheat

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 hasAttempted guard 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_recognition and OpenCV, holding 936 per-roll encodings, with auditable failure logging and saved mismatch frames.
  • Spreadsheet question loading through upload_ques.js, reading ques.xlsx into 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.xlsx and delete_slot1.js.
  • WhatsApp broadcast via a Puppeteer script driving web.whatsapp.com click-to-chat links.
  • Bulk mail through Nodemailer with hand-built Outlook-namespaced XHTML templates.
  • Express hardening with helmet, express-mongo-sanitize, xss-clean, validator and 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

STATUS
Completed
ROLE

Contributor — face-verification service

ORGANISATION

Creative Computing Society

YEAR

2023

SCALE

~2,000 candidates · 936 encodings enrolled

TYPE

Society · Web

TAGS
Proctoring
Assessment
Anti-cheat