# 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://: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` 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": "" } ``` --- ## 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%`). **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": "" } ``` ### GET /api/kurs/:refId/offline Get the `offline` flag of a course. **Response 200** ```json { "offline": 0 } ``` **Response 500** ```json { "status": "error", "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": "" } ``` ### 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": "" }` --- ## 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": "" }` ### 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": "" }` --- ## 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`.