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.Health
GET /api/health
→ DB status, uptime, memory & pool stats
public
Auth — /api/auth
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": "..." // requiredResponses
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
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
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": trueResponses
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": 7Responses
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
Subjects — /api/subjects
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
Request body
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
"name": "...", // optional "code": "...", // optional "absenceThreshold": 25 // optionalResponses
200Subject403Nije vlasnik404Predmet nije pronađen
DELETE
/api/subjects/:id
Soft-delete (deactivate) a subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
200Predmet deaktiviran403Nije vlasnik
PATCH
/api/subjects/:id/activate
Re-activate a deactivated subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
200Predmet aktiviran
GET
/api/subjects/:id/students
List enrolled students for a subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
200Student[]
POST
/api/subjects/:id/enroll
Enroll a single student in a subject
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
"studentId": 7Responses
200Student upisano409Već upisan
POST
/api/subjects/:id/enroll-bulk
Bulk enroll multiple students
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
"studentIds": [7, 8, 12]Responses
200{ "enrolled": 3, "failed": 0 }
DELETE
/api/subjects/:id/enroll/:studentId
Unenroll a student from a subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
| :studentId | Student ID | required |
200Student otpisan404Nije upisan
GET
/api/subjects/:id/assistants
List assistants assigned to a subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
200Assistant[]
POST
/api/subjects/:id/assistants
Assign a co-assistant to a subject
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
"assistantId": 3Responses
201Asistent dodat409Već dodat
DELETE
/api/subjects/:id/assistants/:assistantId
Remove a co-assistant from a subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Subject ID | required |
| :assistantId | Assistant ID | required |
200Asistent uklonjen
Sessions — /api/sessions
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
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
200Session[]
GET
/api/sessions/active
Get the currently active (open QR) session
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
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" // optionalResponses
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
Responses
| Name | Description | |
|---|---|---|
| :id | Session ID | required |
200{ "qrDataUrl": "...", "token": "uuid" }404Termin nije pronađen
POST
/api/sessions/invalidate
Close a session — deactivates QR, keeps attendances
assistant
▼
Request body
"sessionId": 5Responses
200QR poništen, sesija deaktivirana404Termin nije pronađen
DELETE
/api/sessions/:id
Permanently delete a session and its attendances
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Session ID | required |
200Termin je uspešno obrisan403Nije vlasnik subjekta
Attendance — /api/attendance
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
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
200AttendanceRecord[]
GET
/api/attendance/all
All attendance records for a subject
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
200AttendanceRecord[]
GET
/api/attendance/stats
Aggregated stats per session and per student
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
200{ perSession, perStudent, totalSessions, totalStudents }
GET
/api/attendance/matrix
Full session × student presence matrix
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
200AttendanceMatrix
POST
/api/attendance/manual
Manually record one student's attendance
assistant
▼
Request body
"subjectId": 12, "studentId": 7, "sessionId": 5Responses
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
Responses
| Name | Description | |
|---|---|---|
| :id | Attendance ID | required |
| subjectId | Subject ID | required |
200Prisustvo obrisano404Nije pronađeno
Score Sheets — /api/score-sheets
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
Responses
| Name | Description | |
|---|---|---|
| subjectId | Subject ID | required |
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
Responses
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
200SheetFullView404Sheet not found
PATCH
/api/score-sheets/:id/rename
Rename a sheet
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
"name": "Novi naziv"Responses
200ok
DELETE
/api/score-sheets/:id
Delete a sheet with all its columns, rows, and cells
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
200ok403Nije vlasnik
POST
/api/score-sheets/:id/columns
Add a column to a sheet
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
"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
Request body
| Name | Description | |
|---|---|---|
| :colId | Column ID | required |
"name": "...", // optional "maxPoints": 20, // optional "formula": "...", // optional "isHidden": false // optionalResponses
200ok
DELETE
/api/score-sheets/columns/:colId
Delete a column and all its cells
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :colId | Column ID | required |
200ok
POST
/api/score-sheets/:id/columns/reorder
Reorder columns by providing sorted ID array
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
"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 cellResponses
200ok400rowId i columnId su obavezni
POST
/api/score-sheets/:id/rows
Add a row (linked student or manual entry)
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
"studentId": 7, // optional — links enrolled student "studentName": "Ana Petrovic", // optional — manual entry "indexNumber": "PR 42/2023" // optionalResponses
201Row409Student već u tabeli
DELETE
/api/score-sheets/rows/:rowId
Delete a row and all its cells
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :rowId | Row ID | required |
200ok
POST
/api/score-sheets/:id/import-students
Auto-create rows for all enrolled students not yet in the sheet
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Sheet ID | required |
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 }
]
Responses200Transfer 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
Responses
| Name | Description | |
|---|---|---|
| :sheetId | Sheet ID | required |
200StudentSheetView403Student nije u ovoj tabeli
Sheet Versions — /api/versions
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
Responses201Version400Polja su obavezna
GET
/api/versions
List all versions for a sheet (newest first)
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| sheetId | Sheet ID | required |
200Version[]
GET
/api/versions/latest
Get the most recent version of a sheet
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| sheetId | Sheet ID | required |
200Version | null
GET
/api/versions/:id
Get one version by ID
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Version ID | required |
200Version404Verzija nije pronađena
DELETE
/api/versions/:id
Delete a specific version
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Version ID | required |
200Verzija obrisana404Nije pronađena
POST
/api/versions/enforce-limit
Trim old versions keeping only the N newest
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| sheetId | Sheet ID | required |
| maxVersions | Number to keep | required |
200Ograničenje primenjeno
Students — /api/students
GET
/api/students
Paginated list of all students
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| page | Page number | optional |
| limit | Items per page (default 50) | optional |
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
Request body
| Name | Description | |
|---|---|---|
| :id | Student ID | required |
"firstName": "...", // optional "lastName": "...", // optional "email": "...", // optional "smer": "...", // optional "indexNumber": 42, // optional "enrollmentYear": 2023 // optionalResponses
200Student404Student nije pronađen
Assistants — /api/assistants
GET
/api/assistants
List all assistant accounts
assistant
▼
Responses
200Assistant[]
Notifications — /api/notifications
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
Responses
| Name | Description | |
|---|---|---|
| page | Page | optional |
| limit | Limit (default 50) | optional |
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
Responses
| Name | Description | |
|---|---|---|
| :id | Notification ID | required |
200Notifikacija pročitana404Nije pronađena
Reports — /api/reports
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": [...] }
Forms — /api/forms
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": []
}
]
Responses201Form400Naslov je obavezan
GET
/api/forms/:id
Get form details and its questions
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Form ID | required |
200FormWithQuestions403Nije vlasnik404Forma nije pronađena
PUT
/api/forms/:id
Update form metadata and replace questions
assistant
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :id | Form ID | required |
"title": "...", // optional "description": "...", // optional "isActive": false, // optional "questions": [...] // optional — replaces existing questionsResponses
200Forma ažurirana403Nije vlasnik
DELETE
/api/forms/:id
Delete a form and all its responses
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Form ID | required |
200Forma obrisana403Nije vlasnik
GET
/api/forms/:id/responses
Get all submitted responses for a form
assistant
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :id | Form ID | required |
200FormResponse[]403Nije vlasnik
GET
/api/forms/public/:token
Fetch a public form by its share token
public
▼
Parameters
Responses
| Name | Description | |
|---|---|---|
| :token | Public share token | required |
200PublicFormView404Forma nije pronađena
POST
/api/forms/public/:token/submit
Submit a response to a public form
public
▼
Parameters
Request body
| Name | Description | |
|---|---|---|
| :token | Public share token | required |
"answers": {
"q1": "Odgovor tekst",
"q2": ["Opcija A", "Opcija B"]
}
Responses201Odgovor je sačuvan400Odgovori su obavezni404Forma nije aktivna
Maintenance — /api/maintenance
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": 30Responses
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 }