REST Service for POPCORN - ILIAS
alex
14 hours ago 98effcb0b78683294734298d5111a0e299caa8ee
adding doc
1 files added
659 ■■■■■ changed files
REST.md 659 ●●●●● patch | view | raw | blame | history
REST.md
New file
@@ -0,0 +1,659 @@
# 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`.