| New file |
| | |
| | | # REST API |
| | | |
| | | HTTP API of **globus-ilias-rest** — a REST service for POPCORN → ILIAS synchronization. |
| | | |
| | | The service is implemented with [Fastify](https://fastify.dev/) in `app.js` and starts on |
| | | `settings.port` (default `4101`). The active config is `settings.$NODE_ENV.json` |
| | | (see `settings.js`). |
| | | |
| | | ## Base URL |
| | | |
| | | ``` |
| | | http://<host>:4101 |
| | | ``` |
| | | |
| | | In production the service is usually served behind a reverse proxy, e.g. |
| | | `https://globusfm-dev2.minervis.com/popcorn`. |
| | | |
| | | ## Authentication |
| | | |
| | | Every request must pass the auth token as a query parameter: |
| | | |
| | | ``` |
| | | ?token=<authtoken> |
| | | ``` |
| | | |
| | | `authtoken` is defined in `settings.$NODE_ENV.json`. |
| | | |
| | | Requests without a valid token are rejected with `403` after a 500 ms delay |
| | | (anti brute-force / DoS measure): |
| | | |
| | | ```json |
| | | { "status": "error", "error": "access denied" } |
| | | ``` |
| | | |
| | | **Exceptions that do not require a token:** |
| | | |
| | | - `/api/version` |
| | | - everything under `/ui/` (static frontend) |
| | | |
| | | ## Common error format |
| | | |
| | | Most error responses use a consistent shape: |
| | | |
| | | ```json |
| | | { "status": "error", "msg": "not found" } |
| | | ``` |
| | | |
| | | Some routes use `message` or `error` instead of `msg`; this is noted per route. |
| | | |
| | | ## Route overview |
| | | |
| | | | Method | Path | Auth | Purpose | |
| | | |--------|------|------|---------| |
| | | | GET | `/api/version` | no | Service version | |
| | | | GET | `/api/search/user` | yes | Search users (returns usr_ids) | |
| | | | POST | `/api/search/reindex` | yes | Rebuild the user search index | |
| | | | GET | `/api/user` | yes | List/search users (paged) | |
| | | | GET | `/api/user/count` | yes | Total number of (numeric-login) users | |
| | | | GET | `/api/user/login/:login` | yes | Get one user by login | |
| | | | GET | `/api/user/userid/:userid` | yes | Get one user by usr_id | |
| | | | GET | `/api/user/teilnahmen/:userId` | yes | Memberships of a user | |
| | | | POST | `/api/user` | yes | Import/create a user in ILIAS | |
| | | | DELETE | `/api/user/:usr_id` | yes | Delete a user in ILIAS | |
| | | | GET | `/api/ref_id/:ref_id` | yes | Resolve ref_id → obj_id | |
| | | | GET | `/api/obj_id/:obj_id` | yes | Resolve obj_id → ref_id | |
| | | | GET | `/api/kurs` | yes | List all courses | |
| | | | GET | `/api/kurs/:refId` | yes | Get one course | |
| | | | GET | `/api/kurs/items/:refId` | yes | Course item tree (recursive) | |
| | | | GET | `/api/kurs/:refId/teilnehmer` | yes | Course members | |
| | | | GET | `/api/kurs/:refId/teilnehmer/:userId` | yes | Single course member | |
| | | | GET | `/api/kurs/:refId/lp` | yes | Learning progress | |
| | | | GET | `/api/kurs/:refId/teilnehmerByRole` | yes | Course members via role | |
| | | | GET | `/api/kurs/:refId/roles` | yes | Roles assigned in a course | |
| | | | POST | `/api/kurs/:refId/status/:usrId` | yes | Set status + passed for a member | |
| | | | GET | `/api/kurs/:refId/offline` | yes | Get offline flag of a course | |
| | | | POST | `/api/kurs/:refId/offline` | yes | Set offline flag of a course | |
| | | | DELETE | `/api/kurs/:refId/teilnehmer/:usrId` | yes | Unenroll a member (abmelden) | |
| | | | GET | `/api/kurs/rolle/admin` | yes | Courses with admin role | |
| | | | GET | `/api/kurs/rolle/noadmin` | yes | Courses without assigned admin role | |
| | | | GET | `/api/ping` | yes | Ping the ILIAS PHP component | |
| | | | GET | `/ui/*` | no | Built Vue frontend (SPA) | |
| | | |
| | | --- |
| | | |
| | | ## System |
| | | |
| | | ### GET /api/version |
| | | |
| | | Returns the service version from `package.json`. No token required. |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "version": "0.1" } |
| | | ``` |
| | | |
| | | ### GET /api/ping |
| | | |
| | | Pings the custom ILIAS PHP component (`lib/libIlias.js` → `ping`). |
| | | |
| | | **Response 200** — forwarded from the PHP component, e.g. |
| | | |
| | | ```json |
| | | { "method": "GET", "command": "ping", "status": "ok" } |
| | | ``` |
| | | |
| | | **Response 500** |
| | | |
| | | ```json |
| | | { "status": "error", "error": "<error>" } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## Search |
| | | |
| | | ### GET /api/search/user |
| | | |
| | | Full-text search over the user index (`lib/search.js`, FlexSearch). The index is |
| | | built from `login firstname lastname institution department`. |
| | | |
| | | **Query parameters** |
| | | |
| | | | Name | Required | Description | |
| | | |------|----------|-------------| |
| | | | `search` | yes | Search term | |
| | | |
| | | **Response 200** — array of matching `usr_id`s (numbers): |
| | | |
| | | ```json |
| | | [ 23300, 23301 ] |
| | | ``` |
| | | |
| | | **Response 422** — when `search` is missing: |
| | | |
| | | ```json |
| | | { "status": "error", "msg": "no search" } |
| | | ``` |
| | | |
| | | ### POST /api/search/reindex |
| | | |
| | | Rebuilds the user search index from the database. |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "status": "ok", "msg": "reindexed in 231 ms" } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## Users |
| | | |
| | | ### GET /api/user |
| | | |
| | | Paged list / search of users. |
| | | |
| | | **Query parameters** |
| | | |
| | | | Name | Default | Description | |
| | | |------|---------|-------------| |
| | | | `offset` | `0` | Offset | |
| | | | `limit` | `10` | Max results | |
| | | | `search` | — | Optional search term (uses the search index) | |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { |
| | | "total": 11066, |
| | | "offset": 0, |
| | | "limit": 10, |
| | | "data": [ |
| | | { |
| | | "usr_id": 23300, |
| | | "login": "134942", |
| | | "firstname": "Aksana", |
| | | "lastname": "Donhauser", |
| | | "gender": "f", |
| | | "email": "alex@minervis.com", |
| | | "institution": "Globus Baumarkt St. Wendel", |
| | | "street": "", |
| | | "city": "", |
| | | "zipcode": "", |
| | | "country": "", |
| | | "department": "MITARBEITER | FARBEN/TAPETEN/BODENBEL.", |
| | | "active": 1 |
| | | } |
| | | ] |
| | | } |
| | | ``` |
| | | |
| | | > Only users whose `login` is numeric (`login REGEXP '^[0-9]+$'`) are returned. |
| | | > If the query fails, an empty result is returned: |
| | | > `{ "total": 0, "offset": 0, "limit": 0, "data": [] }`. |
| | | |
| | | ### GET /api/user/count |
| | | |
| | | Total number of users (numeric login). Uses `db.getUserCount()` without filters. |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | 11066 |
| | | ``` |
| | | |
| | | ### GET /api/user/login/:login |
| | | |
| | | Get a single user by login. |
| | | |
| | | **Path parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `login` | ILIAS login | |
| | | |
| | | **Response 200** — user object (same fields as `/api/user`) plus all user-defined |
| | | fields merged in as top-level keys. |
| | | |
| | | **Response 404** |
| | | |
| | | ```json |
| | | { "status": "error", "msg": "not found" } |
| | | ``` |
| | | |
| | | ### GET /api/user/userid/:userid |
| | | |
| | | Get a single user by `usr_id`. |
| | | |
| | | **Path parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `userid` | numeric `usr_id` | |
| | | |
| | | **Response 200** — user object incl. user-defined fields. |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | **Response 500** — if `userid` is missing or not numeric: |
| | | |
| | | ```json |
| | | { "status": "error", "msg": "userid error" } |
| | | ``` |
| | | |
| | | ### GET /api/user/teilnahmen/:userId |
| | | |
| | | Memberships of a user (rows from `obj_members` with `member = 1`), enriched with |
| | | object title and learning-progress status. |
| | | |
| | | **Path parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `userId` | numeric `usr_id` | |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "obj_id": 32212, |
| | | "ref_id": 213, |
| | | "usr_id": 6, |
| | | "title": "112 Feuerwehr ist da", |
| | | "status": 2, |
| | | "passed": 1, |
| | | "status_changed": "2025-07-03T11:26:20.000Z" |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | **Response 500** — invalid `userId`: `{ "status": "error", "msg": "userId error" }` |
| | | |
| | | ### POST /api/user |
| | | |
| | | Import (create/update) a user in ILIAS via the custom PHP component. |
| | | |
| | | **Body** (JSON) — POPCORN/SOAP-style user object: |
| | | |
| | | ```json |
| | | { |
| | | "login": "123456789", |
| | | "passwd": "123456789", |
| | | "passwd_type": "plain", |
| | | "firstname": "Adolfo", |
| | | "lastname": "de la Cruz", |
| | | "email": "alex@example.com", |
| | | "gender": "m", |
| | | "department": "Bananenpflücker", |
| | | "institution": "Globus Budapest", |
| | | "role": 4, |
| | | "udf": { |
| | | "Markt": "Markt UDF 2", |
| | | "Marktnummer": "Marktnummer UDF 2", |
| | | "Personalnummer": "Personal UDF 2" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | The keys of `udf` are user-defined-field **names**; they are mapped to field ids |
| | | via `udf_definition` before the request is forwarded to ILIAS. |
| | | |
| | | **Response 200** — result forwarded from the PHP component. |
| | | |
| | | > If a `udf` key does not match a defined field name, the request fails |
| | | > (`udfMap[key]` is `undefined`). |
| | | |
| | | ### DELETE /api/user/:usr_id |
| | | |
| | | Delete a user in ILIAS. |
| | | |
| | | **Path parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `usr_id` | numeric `usr_id` | |
| | | |
| | | **Response 200** — result forwarded from the PHP component. |
| | | **Response 500** — invalid `usr_id`: `{ "status": "error", "msg": "userId error" }` |
| | | |
| | | --- |
| | | |
| | | ## ref_id / obj_id |
| | | |
| | | ### GET /api/ref_id/:ref_id |
| | | |
| | | Resolve a `ref_id` (tree/reference id) to an `obj_id`. |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "ref_id": 213, "obj_id": 32212 } |
| | | ``` |
| | | |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | ### GET /api/obj_id/:obj_id |
| | | |
| | | Resolve an `obj_id` to a `ref_id`. |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "ref_id": 213, "obj_id": 32212 } |
| | | ``` |
| | | |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | --- |
| | | |
| | | ## Courses (Kurs) |
| | | |
| | | ### GET /api/kurs |
| | | |
| | | List all courses (`type = 'crs'`, not deleted). |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "ref_id": 213, |
| | | "obj_id": 32212, |
| | | "title": "112 Feuerwehr ist da", |
| | | "description": "...", |
| | | "type": "crs", |
| | | "offline": 0 |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | ### GET /api/kurs/:refId |
| | | |
| | | Get a single course by `ref_id`. |
| | | |
| | | **Response 200** — single object |
| | | |
| | | ```json |
| | | { |
| | | "ref_id": 213, |
| | | "obj_id": 32212, |
| | | "title": "112 Feuerwehr ist da", |
| | | "description": "...", |
| | | "type": "crs", |
| | | "create_date": "2024-01-01 00:00:00", |
| | | "offline": 0 |
| | | } |
| | | ``` |
| | | |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | ### GET /api/kurs/items/:refId |
| | | |
| | | Recursive tree of items contained in a course (`crs_items`, recursive CTE). |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ |
| | | { "parent_id": 213, "obj_id": 36450, "ref_id": 597, "title": "asdf123", "type": "tst" } |
| | | ] |
| | | ``` |
| | | |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | ### GET /api/kurs/:refId/teilnehmer |
| | | |
| | | Course members. |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "parent_id": 213, |
| | | "ref_id": 597, |
| | | "obj_id": 36450, |
| | | "title": "asdf123", |
| | | "type": "tst", |
| | | "usr_id": 6, |
| | | "login": "root", |
| | | "firstname": "root", |
| | | "lastname": "user", |
| | | "active": 1, |
| | | "passed": 1, |
| | | "status": 2, |
| | | "status_changed": "2025-10-22T07:42:36.000Z" |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | ### GET /api/kurs/:refId/teilnehmer/:userId |
| | | |
| | | Single course member. |
| | | |
| | | **Response 200** — single member object (same fields as above). |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | ### GET /api/kurs/:refId/lp |
| | | |
| | | Learning progress of a course. |
| | | |
| | | **Query parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `raw` | If present (any non-empty value, e.g. `?raw=1`), returns raw sub-object rows instead of the aggregated view | |
| | | |
| | | **Response 200 (raw)** — one row per tracked sub-object and user: |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "usr_id": 6, |
| | | "login": "root", |
| | | "firstname": "root", |
| | | "lastname": "user", |
| | | "obj_id": 32212, |
| | | "item_id": 597, |
| | | "lpmode": 5, |
| | | "item_obj_id": 36450, |
| | | "type": "tst", |
| | | "status": 2, |
| | | "status_changed": "2025-10-22T07:42:36.000Z", |
| | | "percentage": 100, |
| | | "completed": 0 |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | **Response 200 (aggregated)** — one row per user, sub-object statuses combined |
| | | via `lib/libLp.js`: |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "usr_id": 6, |
| | | "login": "root", |
| | | "firstname": "root", |
| | | "lastname": "user", |
| | | "status": 1, |
| | | "status_changed": "2025-10-22T09:45:56.000Z" |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | `status` values: |
| | | |
| | | | Value | Meaning | |
| | | |-------|---------| |
| | | | `0` | not attempted (noch nicht bearbeitet) | |
| | | | `1` | in progress (in Bearbeitung) | |
| | | | `2` | passed (bestanden) | |
| | | | `3` | failed (nicht bestanden) | |
| | | |
| | | **Response 404** — `{ "status": "error", "msg": "not found" }` |
| | | |
| | | ### GET /api/kurs/:refId/teilnehmerByRole |
| | | |
| | | Course members resolved through the course's member role |
| | | (`rbac_ua` joined via the role whose description matches `Member%<obj_id>`). |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ { "role_id": 12345, "usr_id": 6, "firstname": "root", "lastname": "user" } ] |
| | | ``` |
| | | |
| | | ### GET /api/kurs/:refId/roles |
| | | |
| | | Roles assigned in a course (`rbac_pa`). |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "rol_id": 12345, |
| | | "ref_id": 213, |
| | | "obj_id": 999, |
| | | "type": "role", |
| | | "title": "il_crs_member_32212", |
| | | "description": "Member of course 32212" |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | ### POST /api/kurs/:refId/status/:usrId |
| | | |
| | | Set the learning-progress `status` and `passed` flag for a course member. Writes |
| | | both `ut_lp_marks` (status + status_changed) and `obj_members` (passed). |
| | | |
| | | **Path parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `refId` | course `ref_id` | |
| | | | `usrId` | user `usr_id` | |
| | | |
| | | **Body** (JSON) — both fields required |
| | | |
| | | ```json |
| | | { "passed": 1, "status": 2 } |
| | | ``` |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "status": "ok" } |
| | | ``` |
| | | |
| | | **Response 400** — missing arguments: |
| | | |
| | | ```json |
| | | { "status": "error", "msg": "argument error", "statusCode": 400 } |
| | | ``` |
| | | |
| | | **Response 500** — e.g. when the affected rows don't match: |
| | | |
| | | ```json |
| | | { "status": "error", "msg": "<message>" } |
| | | ``` |
| | | |
| | | ### GET /api/kurs/:refId/offline |
| | | |
| | | Get the `offline` flag of a course. |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "offline": 0 } |
| | | ``` |
| | | |
| | | **Response 500** |
| | | |
| | | ```json |
| | | { "status": "error", "message": "<message>" } |
| | | ``` |
| | | |
| | | ### POST /api/kurs/:refId/offline |
| | | |
| | | Set the `offline` flag of a course. |
| | | |
| | | **Body** (JSON) |
| | | |
| | | ```json |
| | | { "offline": 1 } |
| | | ``` |
| | | |
| | | **Response 200** |
| | | |
| | | ```json |
| | | { "offline": 1 } |
| | | ``` |
| | | |
| | | **Response 500** |
| | | |
| | | ```json |
| | | { "status": "error", "message": "<message>" } |
| | | ``` |
| | | |
| | | ### DELETE /api/kurs/:refId/teilnehmer/:usrId |
| | | |
| | | Unenroll a member ("abmelden") via the custom PHP component. |
| | | |
| | | **Path parameters** |
| | | |
| | | | Name | Description | |
| | | |------|-------------| |
| | | | `refId` | course `ref_id` | |
| | | | `usrId` | user `usr_id` | |
| | | |
| | | **Response 200** — result forwarded from the PHP component. |
| | | **Response 404** — `{ "status": "error", "msg": "Teilnahme not found" }` |
| | | **Response 500** — `{ "status": "error", "msg": "<message>" }` |
| | | |
| | | --- |
| | | |
| | | ## Course roles / admins |
| | | |
| | | ### GET /api/kurs/rolle/admin |
| | | |
| | | Courses that have an admin role assigned (role title contains `admin`). |
| | | |
| | | **Response 200** — array of |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "crs_obj_id": 32212, |
| | | "crs_ref_id": 213, |
| | | "crs_title": "112 Feuerwehr ist da", |
| | | "rol_id": 12345, |
| | | "role": "il_crs_admin_32212" |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | **Response 500** — `{ "status": "error", "error": "<error>" }` |
| | | |
| | | ### GET /api/kurs/rolle/noadmin |
| | | |
| | | Courses whose admin role exists but has no user assigned (no `rbac_ua` entry). |
| | | |
| | | **Response 200** — same shape as `/api/kurs/rolle/admin`. |
| | | **Response 500** — `{ "status": "error", "error": "<error>" }` |
| | | |
| | | --- |
| | | |
| | | ## Static frontend |
| | | |
| | | ### GET /ui/* |
| | | |
| | | Serves the built Vue single-page app from `vue/dist` (registered with |
| | | `@fastify/static`, prefix `/ui/`). No token required. |
| | | |
| | | Any route that does not match an API route or a static file returns the SPA |
| | | fallback `vue/dist/index.html`. |