Skip to main content

TutorFlow REST API

The TutorFlow backend is a hand-written REST API built with Node.js and Express. Every endpoint is defined explicitly in backend/routes/ — no endpoint is generated by a scaffolding tool — and every request travels over HTTP(S) as JSON.

Base URL​

EnvironmentBase URL
Production (Azure App Service)https://tutor-api.azurewebsites.net/api
Local developmenthttp://localhost:3000/api

All paths below are relative to the base URL, e.g. GET /users/me means GET https://tutor-api.azurewebsites.net/api/users/me.

Authentication​

The API uses Firebase Authentication. Clients sign in with Firebase (email/password or Google), receive an ID token, and send it with every request:

Authorization: Bearer <firebase-id-token>

Acquiring a token as a web client:

import { getAuth } from 'firebase/auth';

const token = await getAuth().currentUser.getIdToken();
fetch(`${BASE_URL}/users/me`, {
headers: { Authorization: `Bearer ${token}` },
});

The backend verifies every token with the Firebase Admin SDK and loads the caller's database record. Missing, malformed, or expired tokens return:

{ "error": "Invalid Firebase ID token" }

Tokens are short-lived (1 hour) — request a fresh one with getIdToken() rather than caching it long-term.

Roles​

Authorization is role-based. The role is stored on the user record and normalised to uppercase:

RoleWho they areTypical access
STUDENTSigned-up studentsOwn profile, availability, tutoring applications, volunteer views
TUTORApproved tutorsStudent access plus assignments, events, timesheets
LECTURERCourse ownersOwn courses/events, timesheet approval for supervised entries
ORGANISERCourse coordinatorsEverything: applications review, allocation, settings, user directory
ADMINSystem administratorsSame as organiser

Conventions​

  • Format — requests and responses are JSON (Content-Type: application/json), except file uploads (multipart/form-data) and PDF downloads.

  • Status codes — 200 success, 201 created, 204 no content, 400 validation error, 401 missing/invalid token, 403 insufficient role/ownership, 404 not found, 409 conflict.

  • Error envelope — errors raised by controllers share one shape:

    {
    "message": "Only a lecturer, organiser or admin can create a course",
    "path": "/api/courses",
    "timestamp": "2026-09-14T10:12:00.000Z"
    }
  • Timestamps — ISO 8601 UTC strings.

  • Personal data — user endpoints never return the firebaseUid column; profile lookups are restricted to the caller's own record (organisers/admins excepted).

Endpoint Index​

Legend for the Access column: 🔓 no token required, ✅ any authenticated user.

MethodPathAccessPurpose
GET/ (service root)🔓Liveness check for hosting health probes
GET/health (service root)🔓Database connectivity check
GET/usersORGANISER, ADMINUser directory (sanitised fields)
GET/users/me✅Current user's profile
GET/users/verify-role✅Check the caller's role against an allow-list
GET/users/:firebaseUidSelf or ORGANISER/ADMINSingle profile lookup
POST/users🔒 token-verifiedSign up — create own user record
PUT/users✅Update own profile
DELETE/users✅Deactivate own account
GET/courses✅List courses with allocation counts
GET/courses/time-slots✅The bookable time-slot catalogue
POST/coursesLECTURER, ORGANISER, ADMINCreate a course with slots
GET/courses/:courseId✅Course detail with slots and assigned tutors
PUT/courses/:courseIdLECTURER, ORGANISER, ADMINUpdate a course
PUT/courses/:courseId/slotsLECTURER, ORGANISER, ADMINReplace a course's slots
PUT/courses/open-applicationsLECTURER, ORGANISER, ADMINOpen courses for applications
GET/availability✅Own availability grid + locked slots
PUT/availability✅Replace own availability
GET/applicationsSTUDENT, TUTORTutoring opportunities matching availability
GET/applications/mine✅Own course applications
GET/applications/:courseIdSTUDENT, TUTOROne opportunity's details
POST/applications/:courseId/applySTUDENT, TUTORApply with a course mark
POST/onboarding/apply✅Apply for the TUTOR or LECTURER role
GET/onboarding/tutor-eligible-courses✅Courses that fit the caller's availability
GET/onboarding/my-applications✅Own role applications
GET/onboarding/applicationsORGANISERPending role applications
GET/onboarding/applications/:idOrganiser or ownerRole application detail
GET/onboarding/applications/:id/rankORGANISERRank suggestion for a tutor applicant
PATCH/onboarding/applications/:id/reviewORGANISERApprove/reject a role application
GET/allocation/available-tutorsORGANISER, ADMINApplicant pool ranked by mark
GET/allocation/assignmentsORGANISER, ADMINCurrent tutor assignments
POST/allocation/assignmentsORGANISER, ADMINAssign a tutor to a course
DELETE/allocation/assignments/:applicantIdORGANISER, ADMINUnassign (returns applicant to pool)
DELETE/allocation/assignments/:applicantId/removeORGANISER, ADMINRemove permanently (rejects applicant)
GET/eventsTUTOR, LECTURER, ORGANISER, ADMINEvents visible to the caller's role
GET/events/my-coursesLECTUREROwn courses for event creation
GET/events/claimableTUTORPast unclaimed events in the claim window
POST/eventsLECTURERCreate an event on an own course
GET/events/:idRole-scopedSingle event
PUT/events/:idLECTURER (own course)Update an event
GET/swaps/my-sessionsTUTOROwn sessions available for swapping
GET/swaps/candidates/:eventIdTUTOREligible swap partners for a session, with constraint verdicts
GET/swapsTUTOR, ORGANISER, ADMINSwap requests (own for tutors, all for staff, ?status= filter)
POST/swapsTUTORRequest a session swap (one-way or two-way)
POST/swaps/:id/acceptRecipient TUTORAgree to the trade
POST/swaps/:id/declineRecipient TUTORDecline with an optional reason
POST/swaps/:id/cancelRequester TUTORWithdraw a pending request
POST/swaps/:id/approveORGANISER, ADMINApprove — re-validates constraints, then moves the claims
POST/swaps/:id/rejectORGANISER, ADMINReject an agreed swap with an optional reason
GET/timesheets✅Own timesheets
GET/timesheets/pending-reviewLECTURER, ORGANISER, ADMINSubmitted timesheets to review
GET/timesheets/supervisors✅Supervisor directory for entry forms
POST/timesheets✅Create a draft timesheet
GET/timesheets/:idOwner/supervisor/staffSingle timesheet
PUT/timesheets/:idOwnerReplace entries by claiming events
DELETE/timesheets/:idOwner (draft only)Delete a draft timesheet
POST/timesheets/:id/submitOwnerSubmit for approval
PUT/timesheets/:id/entries/:entryId/approveSupervising staffApprove one entry
PUT/timesheets/:id/rejectSupervising staffReject with a reason
GET/timesheets/:id/export✅Download an approved timesheet as PDF
GET/settings✅System settings (hours cap, claim window, rates)
PUT/settings/tutor-monthly-hoursORGANISER, ADMINSet the monthly claimable-hours cap
PUT/settings/tutor-event-claim-monthsORGANISER, ADMINSet the event claim window
PUT/settings/hourly-ratesORGANISER, ADMINSet hourly rates per year of study
PUT/settings/pdf-templateORGANISER, ADMINUpload the timesheet PDF template
GET/settings/pdf-templateORGANISER, ADMINDownload the template
DELETE/settings/pdf-templateORGANISER, ADMINRemove the template

Users​

GET /users — user directory​

Organiser/admin view of every registered account. Responses contain only profile fields — id, studentNumber, employeeNumber, firstName, lastName, email, phoneNumber, yearOfStudy, role, isActive, createdAt, updatedAt. The Firebase identifier is never exposed.

GET /users/me — own profile​

Returns the caller's profile in the same sanitised shape as above.

GET /users/verify-role?roles=TUTOR,LECTURER — role gate check​

Returns { "role": "TUTOR", "allowed": true } when the caller's role is in the comma-separated allow-list, otherwise 403. The frontend route guards use this to reject direct navigation to another role's page server-side.

GET /users/:firebaseUid — single profile​

Fetches any profile by the caller's own Firebase UID. Access is enforced in the controller: a caller may only look up their own record; organisers and admins may look up anyone.

POST /users — sign up​

Creates the caller's own user record after Firebase account creation. The request must carry the caller's Firebase ID token, and the token's UID (and email, when present) must match the body:

{
"firstName": "Alan",
"lastName": "Turing",
"email": "alan.turing@wits.ac.za",
"firebaseUid": "the-firebase-uid-of-the-caller"
}

Responds 201 with the created profile. A token belonging to a different Firebase account, or an email mismatch, returns 403; an already-registered email or UID returns 409. New users get the STUDENT role.

PUT /users — update own profile​

Editable fields only: firstName, lastName, phoneNumber, yearOfStudy (one of 3rd year, honours, masters, phd). Email and department are read-only.

DELETE /users — deactivate own account​

Soft-deletes the caller: sets isActive: false in the database and disables the Firebase account. Responds 204.

Courses​

GET /courses​

All courses with allocation progress (assignedCount, tutorsRequired, lecturer name).

POST /courses​

Creates a course with its weekly tutoring slots in one atomic call:

{
"courseCode": "COMS3007",
"courseName": "Machine Learning",
"minimumMark": 70,
"tutorsRequired": 2,
"slots": [{ "dayOfWeek": 1, "timeSlotId": 3 }]
}

dayOfWeek is 0 (Monday) to 4 (Friday); timeSlotId references the time-slot catalogue. Duplicate course codes return 409.

PUT /courses/:courseId and PUT /courses/:courseId/slots​

Full course update and slot replacement respectively — both validated the same way as creation.

PUT /courses/open-applications​

Opens the application process for the listed courses: { "courseIds": [1, 2] }. Courses that already have tutors assigned or are already open are skipped, and the response reports requestedCount, updatedCount and skippedCount.

Availability​

GET /availability​

The caller's availability grid plus the time-slot catalogue and slots locked by submitted applications: { timeSlots, availability, lockedSlots }.

PUT /availability​

Replaces the caller's availability wholesale: { "availability": [{ "dayOfWeek": 1, "timeSlotId": 3 }] }. Locked slots (held by submitted applications) must remain in the set — removing them returns 400.

Applications (tutoring opportunities)​

Available to students and tutors. Only courses whose tutoring slots fit inside the caller's availability are returned.

GET /applications / GET /applications/:courseId​

Opportunity lists/detail with weekly claimable hours and hours-cap warnings (hoursExceeded).

POST /applications/:courseId/apply​

Body: { "courseMark": 82 }. The server enforces the course's minimum mark, availability coverage, no duplicate applications (409), and the organiser-set claimable-hours cap.

Onboarding (role applications)​

POST /onboarding/apply​

Submits an application for the TUTOR or LECTURER role.

GET /onboarding/applications / GET /onboarding/applications/:id/rank / PATCH /onboarding/applications/:id/review​

Organiser tools: the pending queue, a rank suggestion computed from course marks and availability, and approve/reject ({ "action": "approve" } or { "action": "reject", "reason": "..." }). Approving a tutor application promotes the user to TUTOR and creates their tutor record.

Allocation​

All allocation endpoints are organiser/admin only — they expose applicant personal data and mutate assignments.

GET /allocation/available-tutors?courseId=…​

Applicants ranked by course mark (descending), excluding already-assigned applicants.

POST /allocation/assignments​

Body: { "applicantId": 12, "allocatedHours": 5, "semester": "2026-S2" }. Creates (or reuses) the tutor record and the assignment transactionally, then fires a notification email. Enforces the course's tutor cap (409 on overflow).

DELETE /allocation/assignments/:applicantId and .../remove​

Unassign returns the applicant to the pending pool; remove permanently rejects them. Both send an email notification and respond 204.

Events​

GET /events​

Role-scoped: tutors see events on their allocated courses, lecturers see events on courses they created, organisers/admins see everything.

POST /events / PUT /events/:id​

Lecturer-only, own courses only. Body: { "courseId": 3, "date": "2026-09-21", "description": "tutoring" } where description is one of tutoring, marking, invigilation, admin. One event per course per date — duplicates return 409.

GET /events/claimable​

Tutor-only: past, unclaimed events on the tutor's allocated courses, inside the organiser-set claim window.

Timesheets​

GET /timesheets / POST /timesheets​

Own timesheets, and creating a draft (optional body { "name": "September claim" }).

PUT /timesheets/:id​

Replaces all entries by claiming events — the body only carries event IDs:

{ "name": "September claim", "entries": [{ "eventId": 41 }, { "eventId": 42 }] }

Course, date, supervisor and duration are derived server-side from the claimed event; manual entries are rejected. Every event can be claimed exactly once (409 on a taken event).

POST /timesheets/:id/submit​

Submits for approval; enforces the monthly claimable-hours cap per rolling 31-day window.

PUT /timesheets/:id/entries/:entryId/approve / PUT /timesheets/:id/reject​

Supervising lecturer (or organiser/admin) actions. Rejection requires a reason and resets the timesheet so the tutor can edit and resubmit. A timesheet becomes APPROVED once every entry is approved.

GET /timesheets/:id/export​

Renders an approved timesheet onto the uploaded PDF template and streams it back as application/pdf.

Session Swaps​

Tutors trade claimed sessions (TimesheetEntry claims on events) peer-to-peer. The platform validates every swap against the same constraints used when a tutor is allocated — course allocation, availability coverage, busy blocks, double-booking and the monthly claimable-hours cap — and the organiser gives the final approval before any session moves.

Status flow: PENDING → (recipient accepts) → ACCEPTED → (organiser approves) → APPROVED. The recipient can decline and the organiser can reject (both land on REJECTED), and the requester can CANCEL while the request is still PENDING.

GET /swaps/my-sessions / GET /swaps/candidates/:eventId​

Tutor-only. The first lists the caller's swappable sessions (pending claimed events). The second returns every other tutor allocated to the same course with an eligible flag, the blocking constraint reasons, and that tutor's own swappable sessions (for two-way trades).

POST /swaps​

Body: { "recipientId": 20, "requesterEventId": 100, "recipientEventId": 200, "reason": "…" }. recipientEventId is optional — omit it for a one-way handover. Every constraint violation returns 403 with the reasons; a session with a live request returns 409.

POST /swaps/:id/accept|decline|cancel​

Recipient-side responses and the requester's withdrawal, each allowed only while the request is PENDING. Declining and rejecting accept an optional free-text reason.

POST /swaps/:id/approve|reject​

Organiser/admin review of ACCEPTED requests. Approval re-validates constraints against current data — when anything no longer passes, the failure returns 409 and nothing moves. On success the session's claim (the unique TimesheetEntry.eventId row) is transferred to the receiving tutor's newest draft timesheet (a fresh draft is opened when none exists) and both affected timesheet totals are recomputed, all inside one transaction.

Settings​

GET /settings​

Current system settings: tutorMonthlyClaimableHours, tutorEventClaimMonths, hourlyRates keyed by year of study, and pdfTemplateFilename.

PUT /settings/…​

Organiser/admin configuration updates (hours cap, claim window, hourly rates, PDF template upload via multipart/form-data field template). Values are validated server-side with sensible bounds (e.g. hourly rates 0–5000 ZAR).

Service Health​

Two unauthenticated endpoints exist purely for infrastructure monitoring and expose no user data:

  • GET / — {"message":"TutorFlow API is running","timestamp":"…"} (Azure health probe)
  • GET /health — pings the database with SELECT 1, returning healthy or 503 unhealthy

External Service Integrations​

The API integrates with two external services over HTTPS:

  1. Firebase Authentication (Google Identity Platform) — every request's bearer token is verified server-side with the Firebase Admin SDK; account deactivation propagates back by disabling the Firebase account.
  2. Resend (email delivery) — transactional notification emails (assignment added/removed, timesheet approved/rejected, swap requested/responded/reviewed) are sent through the Resend HTTP API, with every attempt recorded and de-duplicated in the EmailLog table.