Tapiz REST API

Academic course management API — attendance, score sheets, QR sessions, notifications, forms, and more.

Hono + TypeScriptPostgreSQL + TypeORM JWT AuthValkey rate limitingVercel Serverless
assistantJWT role: assistant studentJWT role: student anyJWT — any role publicNo auth required
All authenticated routes require Authorization: Bearer <token>. Obtain tokens from POST /api/auth/login, refresh via POST /api/auth/refresh.
GET /api/health → DB status, uptime, memory & pool stats public
POST /api/auth/register Register new student public
Request body
"smer":           "PR",        // required
"indexNumber":    "42",        // required
"enrollmentYear": "2023",      // required
"lastName":       "Petrovic",  // required
"firstName":      "Ana",       // required
"email":          "ana@edu.rs",// required
"password":       "..."        // required
Responses
201Registracija uspešna400Sva polja su obavezna409Email već postoji
POST /api/auth/login Login — returns JWT access token public
⚡ Rate limited — 10 req / 15 min per IP
Request body
"email":    "ana@edu.rs",
"password": "..."
Responses
200{ "accessToken": "...", "role": "student|assistant" }401Pogrešni kredencijali429Rate limit exceeded
POST /api/auth/verify-2fa Complete 2FA login with 6-digit email code public
⚡ Rate limited — 5 req / 15 min per IP
Request body
"email": "ana@edu.rs",
"code":  "123456"
Responses
200{ "accessToken": "..." }403Pogrešan ili istekao kod429Rate limit exceeded
GET /api/auth/2fa-status Get auth user's 2FA enabled flag any
Responses
200{ "enabled": true }
POST /api/auth/toggle-2fa Enable or disable 2FA for auth user any
Request body
"enabled": true
Responses
2002FA je uključena / isključena400enabled mora biti boolean
POST /api/auth/refresh Refresh JWT access token any
Responses
200{ "accessToken": "..." }
GET /api/auth/assistant-profile Get auth assistant's own profile assistant
Responses
200AssistantProfile
POST /api/auth/change-password-assistant Change assistant's own password assistant
Request body
"currentPassword": "...",
"newPassword":     "..."
Responses
200Lozinka je uspešno promenjena400Sva polja su obavezna401Pogrešna lozinka
POST /api/auth/deactivate-account Deactivate assistant's own account assistant
Request body
"password": "..."
Responses
200Nalog je deaktiviran
POST /api/auth/reset-student-password Generate new temporary password for a student assistant
Request body
"studentId": 7
Responses
200New temp password message404Student nije pronađen
GET /api/auth/student-profile Get auth student's own profile student
Responses
200StudentProfile
POST /api/auth/change-password Change student's own password student
Request body
"currentPassword": "...",
"newPassword":     "..."
Responses
200Lozinka je uspešno promenjena401Pogrešna lozinka
POST /api/auth/deactivate-student-account Deactivate student's own account student
Request body
"password": "..."
Responses
200Nalog je deaktiviran
POST /api/auth/seed-assistant Bootstrap first assistant account (one-time setup) public
Request body
"email":     "admin@edu.rs",
"password":  "...",
"firstName": "Marko",
"lastName":  "Jovic",
"secretKey": ""
Responses
201Asistent kreiran403Pogrešan secretKey
GET /api/subjects List all subjects any
Responses
200Subject[]
GET /api/subjects/my List subjects owned by auth assistant assistant
Responses
200Subject[]
GET /api/subjects/my/inactive List deactivated subjects of auth assistant assistant
Responses
200Subject[]
GET /api/subjects/enrolled Subjects the auth user is enrolled in any
Responses
200Subject[]
POST /api/subjects Create a new subject assistant
Request body
"name":             "Programiranje 1", // required
"code":             "P1",             // required
"absenceThreshold": 30                // optional — max absence % (default 30)
Responses
201Subject400Naziv i šifra su obavezni
PATCH /api/subjects/:id Update subject properties assistant
Parameters
NameDescription
:idSubject ID required
Request body
"name":             "...", // optional
"code":             "...", // optional
"absenceThreshold": 25    // optional
Responses
200Subject403Nije vlasnik404Predmet nije pronađen
DELETE /api/subjects/:id Soft-delete (deactivate) a subject assistant
Parameters
NameDescription
:idSubject ID required
Responses
200Predmet deaktiviran403Nije vlasnik
PATCH /api/subjects/:id/activate Re-activate a deactivated subject assistant
Parameters
NameDescription
:idSubject ID required
Responses
200Predmet aktiviran
GET /api/subjects/:id/students List enrolled students for a subject assistant
Parameters
NameDescription
:idSubject ID required
Responses
200Student[]
POST /api/subjects/:id/enroll Enroll a single student in a subject assistant
Parameters
NameDescription
:idSubject ID required
Request body
"studentId": 7
Responses
200Student upisano409Već upisan
POST /api/subjects/:id/enroll-bulk Bulk enroll multiple students assistant
Parameters
NameDescription
:idSubject ID required
Request body
"studentIds": [7, 8, 12]
Responses
200{ "enrolled": 3, "failed": 0 }
DELETE /api/subjects/:id/enroll/:studentId Unenroll a student from a subject assistant
Parameters
NameDescription
:idSubject ID required
:studentIdStudent ID required
Responses
200Student otpisan404Nije upisan
GET /api/subjects/:id/assistants List assistants assigned to a subject assistant
Parameters
NameDescription
:idSubject ID required
Responses
200Assistant[]
POST /api/subjects/:id/assistants Assign a co-assistant to a subject assistant
Parameters
NameDescription
:idSubject ID required
Request body
"assistantId": 3
Responses
201Asistent dodat409Već dodat
DELETE /api/subjects/:id/assistants/:assistantId Remove a co-assistant from a subject assistant
Parameters
NameDescription
:idSubject ID required
:assistantIdAssistant ID required
Responses
200Asistent uklonjen
Valid session types: Predavanja · Računarske vežbe · Auditorne vežbe · Labaratorijske vežbe
GET /api/sessions List all sessions for a subject assistant
Parameters
NameDescription
subjectIdSubject ID required
Responses
200Session[]
GET /api/sessions/active Get the currently active (open QR) session assistant
Parameters
NameDescription
subjectIdSubject ID required
Responses
200Session | null
POST /api/sessions/generate Create a session and generate its QR code assistant
Request body
"subjectId":     12,                 // required
"sessionNumber": 3,                  // required
"sessionType":   "Računarske vežbe"  // optional
Responses
200{ "qrDataUrl": "data:image/png...", "session": {...}, "expiresInSeconds": 300 }400Neispravan tip termina
POST /api/sessions/rotate/:id Rotate QR UUID mid-session (no expiry change) assistant
Parameters
NameDescription
:idSession ID required
Responses
200{ "qrDataUrl": "...", "token": "uuid" }404Termin nije pronađen
POST /api/sessions/invalidate Close a session — deactivates QR, keeps attendances assistant
Request body
"sessionId": 5
Responses
200QR poništen, sesija deaktivirana404Termin nije pronađen
DELETE /api/sessions/:id Permanently delete a session and its attendances assistant
Parameters
NameDescription
:idSession ID required
Responses
200Termin je uspešno obrisan403Nije vlasnik subjekta
POST /api/attendance/scan Student scans QR code to record attendance student
Request body
"sessionId": 5,
"token":     "uuid-from-qr"
Responses
200Attendance record400Token istekao / Neispravan token403Student nije upisan409Prisustvo već evidentirano
GET /api/attendance/my Student: own attendance for a subject student
Parameters
NameDescription
subjectIdSubject ID required
Responses
200AttendanceRecord[]
GET /api/attendance/all All attendance records for a subject assistant
Parameters
NameDescription
subjectIdSubject ID required
Responses
200AttendanceRecord[]
GET /api/attendance/stats Aggregated stats per session and per student assistant
Parameters
NameDescription
subjectIdSubject ID required
Responses
200{ perSession, perStudent, totalSessions, totalStudents }
GET /api/attendance/matrix Full session × student presence matrix assistant
Parameters
NameDescription
subjectIdSubject ID required
Responses
200AttendanceMatrix
POST /api/attendance/manual Manually record one student's attendance assistant
Request body
"subjectId": 12,
"studentId": 7,
"sessionId": 5
Responses
201Attendance record409Već evidentirano
POST /api/attendance/manual-bulk Bulk manually record attendance for multiple students assistant
Request body
"subjectId":  12,
"sessionId":  5,
"studentIds": [7, 8, 12]
Responses
201{ "added": [...], "failed": 0 }
DELETE /api/attendance/:id Delete a single attendance record assistant
Parameters
NameDescription
:idAttendance ID required
subjectIdSubject ID required
Responses
200Prisustvo obrisano404Nije pronađeno
Column types: number — plain numeric; formula — server-side computed (supports SUM, IF, col{N} references). Formula columns are highlighted in teal in the PDF export.
GET /api/score-sheets List all sheets for a subject assistant
Parameters
NameDescription
subjectIdSubject ID required
Responses
200ScoreSheet[]
POST /api/score-sheets Create a new score sheet assistant
Request body
"subjectId":    12,
"name":         "Kolokvijum 1",
"academicYear": "2024/25"
Responses
201ScoreSheet400Polja su obavezna
GET /api/score-sheets/:id Get full sheet with columns, rows, and computed cells assistant
Parameters
NameDescription
:idSheet ID required
Responses
200SheetFullView404Sheet not found
PATCH /api/score-sheets/:id/rename Rename a sheet assistant
Parameters
NameDescription
:idSheet ID required
Request body
"name": "Novi naziv"
Responses
200ok
DELETE /api/score-sheets/:id Delete a sheet with all its columns, rows, and cells assistant
Parameters
NameDescription
:idSheet ID required
Responses
200ok403Nije vlasnik
POST /api/score-sheets/:id/columns Add a column to a sheet assistant
Parameters
NameDescription
:idSheet ID required
Request body
"name":      "Domaci 1",    // required
"type":      "number",      // required: "number" | "formula"
"maxPoints": 10,            // optional (number columns)
"formula":   "SUM(col1)"   // optional (formula columns)
Responses
201Column400name i type su obavezni
PATCH /api/score-sheets/columns/:colId Update column name, maxPoints, formula, or visibility assistant
Parameters
NameDescription
:colIdColumn ID required
Request body
"name":      "...",  // optional
"maxPoints": 20,     // optional
"formula":   "...",  // optional
"isHidden":  false   // optional
Responses
200ok
DELETE /api/score-sheets/columns/:colId Delete a column and all its cells assistant
Parameters
NameDescription
:colIdColumn ID required
Responses
200ok
POST /api/score-sheets/:id/columns/reorder Reorder columns by providing sorted ID array assistant
Parameters
NameDescription
:idSheet ID required
Request body
"orderedIds": [3, 1, 2]
Responses
200ok
PUT /api/score-sheets/cells Set a single cell value assistant
Request body
"rowId":    14,
"columnId": 3,
"value":    "8.5"  // empty string clears the cell
Responses
200ok400rowId i columnId su obavezni
POST /api/score-sheets/:id/rows Add a row (linked student or manual entry) assistant
Parameters
NameDescription
:idSheet ID required
Request body
"studentId":   7,              // optional — links enrolled student
"studentName": "Ana Petrovic", // optional — manual entry
"indexNumber": "PR 42/2023"   // optional
Responses
201Row409Student već u tabeli
DELETE /api/score-sheets/rows/:rowId Delete a row and all its cells assistant
Parameters
NameDescription
:rowIdRow ID required
Responses
200ok
POST /api/score-sheets/:id/import-students Auto-create rows for all enrolled students not yet in the sheet assistant
Parameters
NameDescription
:idSheet ID required
Responses
200{ "imported": 12, "skipped": 3 }
POST /api/score-sheets/transfer Copy column values between two sheets with a column mapping assistant
Request body
"sourceSheetId": 1,
"targetSheetId": 2,
"columnMapping": [
  { "sourceColumnId": 3, "targetColumnId": 7 }
]
Responses
200Transfer summary400Polja su obavezna
GET /api/score-sheets/student/subjects Student: list subjects that have at least one sheet student
Responses
200SubjectWithSheets[]
GET /api/score-sheets/student/sheet/:sheetId Student: read-only view of own row in a sheet student
Parameters
NameDescription
:sheetIdSheet ID required
Responses
200StudentSheetView403Student nije u ovoj tabeli
Versions are full JSONB snapshots of a sheet's state. Stored newest-first. Use enforce-limit to prune old entries.
POST /api/versions Save a named snapshot of a sheet assistant
Request body
"sheetId":  1,
"label":    "Pre kolokvijuma",
"snapshot": { ...full sheet JSON... }  // required, valid object
Responses
201Version400Polja su obavezna
GET /api/versions List all versions for a sheet (newest first) assistant
Parameters
NameDescription
sheetIdSheet ID required
Responses
200Version[]
GET /api/versions/latest Get the most recent version of a sheet assistant
Parameters
NameDescription
sheetIdSheet ID required
Responses
200Version | null
GET /api/versions/:id Get one version by ID assistant
Parameters
NameDescription
:idVersion ID required
Responses
200Version404Verzija nije pronađena
DELETE /api/versions/:id Delete a specific version assistant
Parameters
NameDescription
:idVersion ID required
Responses
200Verzija obrisana404Nije pronađena
POST /api/versions/enforce-limit Trim old versions keeping only the N newest assistant
Parameters
NameDescription
sheetIdSheet ID required
maxVersionsNumber to keep required
Responses
200Ograničenje primenjeno
GET /api/students Paginated list of all students assistant
Parameters
NameDescription
pagePage number optional
limitItems per page (default 50) optional
Responses
200{ data: Student[], total, page, limit }
GET /api/students/me Student: get own full profile student
Responses
200StudentProfile
PUT /api/students/:id Update student fields assistant
Parameters
NameDescription
:idStudent ID required
Request body
"firstName":      "...", // optional
"lastName":       "...", // optional
"email":          "...", // optional
"smer":           "...", // optional
"indexNumber":    42,    // optional
"enrollmentYear": 2023   // optional
Responses
200Student404Student nije pronađen
GET /api/assistants List all assistant accounts assistant
Responses
200Assistant[]
Auto-generated on QR scan: ABSENCE_WARNING (≤ 2 absences remaining) and THRESHOLD_EXCEEDED (over limit). Only one unread notification per student / subject / type.
GET /api/notifications Student: paginated notification list student
Parameters
NameDescription
pagePage optional
limitLimit (default 50) optional
Responses
200{ data: Notification[], total, unread }
GET /api/notifications/unread-count Student: count unread notifications student
Responses
200{ "count": 3 }
POST /api/notifications/read-all Student: mark all notifications as read student
Responses
200Sve označene kao pročitane
PATCH /api/notifications/:id/read Student: mark one notification as read student
Parameters
NameDescription
:idNotification ID required
Responses
200Notifikacija pročitana404Nije pronađena
Cached in Valkey for 5 minutes per assistant. Aggregates attendance across all subjects the assistant owns.
GET /api/reports/weekly Attendance report — last 7 days assistant
Responses
200{ "period": "weekly", "reports": [...] }
GET /api/reports/monthly Attendance report — last 30 days assistant
Responses
200{ "period": "monthly", "reports": [...] }
GET /api/forms List all forms created by auth assistant assistant
Responses
200Form[]
POST /api/forms Create a form with optional questions assistant
Request body
"title":       "Anketa",     // required
"description": "...",        // optional
"questions": [               // optional — can be added later
  {
    "label":    "Kako ocenjujete?",
    "type":     "text",
    "position": 1,
    "options":  []
  }
]
Responses
201Form400Naslov je obavezan
GET /api/forms/:id Get form details and its questions assistant
Parameters
NameDescription
:idForm ID required
Responses
200FormWithQuestions403Nije vlasnik404Forma nije pronađena
PUT /api/forms/:id Update form metadata and replace questions assistant
Parameters
NameDescription
:idForm ID required
Request body
"title":       "...",  // optional
"description": "...",  // optional
"isActive":    false,  // optional
"questions":   [...]   // optional — replaces existing questions
Responses
200Forma ažurirana403Nije vlasnik
DELETE /api/forms/:id Delete a form and all its responses assistant
Parameters
NameDescription
:idForm ID required
Responses
200Forma obrisana403Nije vlasnik
GET /api/forms/:id/responses Get all submitted responses for a form assistant
Parameters
NameDescription
:idForm ID required
Responses
200FormResponse[]403Nije vlasnik
GET /api/forms/public/:token Fetch a public form by its share token public
Parameters
NameDescription
:tokenPublic share token required
Responses
200PublicFormView404Forma nije pronađena
POST /api/forms/public/:token/submit Submit a response to a public form public
Parameters
NameDescription
:tokenPublic share token required
Request body
"answers": {
  "q1": "Odgovor tekst",
  "q2": ["Opcija A", "Opcija B"]
}
Responses
201Odgovor je sačuvan400Odgovori su obavezni404Forma nije aktivna
State stored in Valkey. The activating assistant bypasses the maintenance gate. All other users are blocked until the window expires or is manually stopped.
GET /api/maintenance/status Poll current maintenance state public
Responses
200{ "isActive": false, "expiresAt": null, "activatedBy": null }
POST /api/maintenance/start Activate a timed maintenance window (1–60 min) assistant
Request body
"durationMinutes": 30
Responses
201{ "isActive": true, "expiresAt": "...", "activatedBy": 2 }400durationMinutes mora biti između 1 i 60
POST /api/maintenance/stop Immediately end the active maintenance window assistant
Responses
200{ "isActive": false }