badmfck-api-server 5.0.8 → 5.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ # Monitoring API — Frontend Guide
2
+
3
+ Интеграция фронта с эндпоинтами мониторинга `badmfck-api-server`. Все эндпоинты живут под префиксом **`sm`**: полный путь `‹baseEndPoint›/sm/‹endpoint›` (обычно `/api/sm/…`).
4
+
5
+ > **Версия с обязательной 2FA.** Старая авторизация статическим токеном `sha256(login:password)` **удалена**. Доступ только через сессию, полученную в `sm/login`.
6
+
7
+ ---
8
+
9
+ ## 1. Вход, 2FA и сессия
10
+
11
+ ### Шаг 1 — логин
12
+
13
+ ```js
14
+ POST sm/login { login, password } // + totp, когда 2FA уже подключена
15
+ ```
16
+
17
+ Возможны два ответа.
18
+
19
+ **A. 2FA ещё не подключена** — сессия не выдаётся, `sm/*` недоступен:
20
+
21
+ ```jsonc
22
+ {
23
+ "enrollmentRequired": true,
24
+ "enrollmentToken": "…", // понадобится на шаге 2, живёт 10 минут
25
+ "secret": "JBSWY3DPEHPK3PXP", // для ручного ввода, если нет камеры
26
+ "otpauthUri": "otpauth://totp/‹projectName›:ops?secret=…&issuer=‹projectName›&algorithm=SHA1&digits=6&period=30",
27
+ "expiresInMs": 600000
28
+ }
29
+ ```
30
+
31
+ **B. 2FA подключена и код верный** — обычная сессия:
32
+
33
+ ```jsonc
34
+ { "token": "…", "login": "ops", "idleTimeoutMs": 900000, "maxLifetimeMs": 28800000,
35
+ "twoFactor": true, "role": "it", "capabilities": ["dashboard","logs"] }
36
+ ```
37
+
38
+ ### Шаг 2 — подключение 2FA (только при ответе A)
39
+
40
+ **Нарисуй `otpauthUri` QR-кодом** (любая JS-библиотека QR) и покажи рядом `secret` строкой для ручного ввода. Пользователь сканирует код в Google Authenticator и вводит 6 цифр:
41
+
42
+ > В аутентификаторе запись подпишется именем проекта — `issuer` берётся из `projectName` на бэке. Поэтому несколько проектов на одном сервере видны в приложении как отдельные строки и не путаются между собой. Задай `projectName` осмысленно: при значении по умолчанию все проекты будут называться одинаково.
43
+
44
+ ```js
45
+ POST sm/enroll2fa { enrollmentToken, totp }
46
+ ```
47
+
48
+ Ответ — сразу рабочая сессия (та же форма, что B). Секрет сохраняется на сервере **только после** верного кода.
49
+
50
+ ### Шаг 3 — запросы
51
+
52
+ ```
53
+ Authorization: Bearer ‹token из login›
54
+ ```
55
+
56
+ ### Жизнь сессии
57
+
58
+ - **Idle-таймаут 15 минут** (`idleTimeoutMs`): каждый успешный запрос продлевает окно. Истекла — ответ `401`, веди на экран логина.
59
+ - **Максимум 8 часов** (`maxLifetimeMs`) независимо от активности.
60
+ - `POST sm/logout` — отозвать немедленно.
61
+ - **10 неудачных попыток → блокировка аккаунта на 30 минут.** Сообщение приходит в `error.details`, покажи его как есть.
62
+
63
+ ### Правила для UI
64
+
65
+ - Токен держи **в памяти вкладки**, не в localStorage.
66
+ - Повторное подключение 2FA через API **невозможно** — это защита от перепривязки чужим человеком, узнавшим пароль. Потерян телефон → администратор удаляет запись в файле 2FA на сервере.
67
+ - Пароль короче 12 символов настроить нельзя: вход такой учёткой вернёт `password policy violation` с причиной.
68
+
69
+ ```js
70
+ async function sm(endpoint, body = {}, token) {
71
+ const res = await fetch(`/api/sm/${endpoint}`, {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
74
+ body: JSON.stringify(body),
75
+ });
76
+ const json = await res.json();
77
+ if (json.error) throw Object.assign(new Error(json.error.message), json.error);
78
+ return json.data;
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 2. Роли
85
+
86
+ У каждой учётки есть роль. Она приходит в ответе логина (`role`, `capabilities`) — **по ней и прячь разделы интерфейса**, не дожидаясь `403`.
87
+
88
+ | Роль | Дашборд | Логи | Рестарт / уровень логирования |
89
+ |---|---|---|---|
90
+ | `admin` | ✅ | ✅ | ✅ |
91
+ | `it` | ✅ | ✅ | ⛔ |
92
+ | `analytic` | ✅ | ⛔ | ⛔ |
93
+
94
+ Попытка зайти не по правам — `403` с текстом `Role "analytic" is not allowed to access logs endpoints`.
95
+
96
+ ---
97
+
98
+ ## 3. Формат ответа
99
+
100
+ **Успех:**
101
+
102
+ ```jsonc
103
+ { "data": /* полезная нагрузка */, "responseTime": 4, "core": "5.0.8",
104
+ "endpoint": "/api/sm/metrics", "version": "6.0.7" }
105
+ ```
106
+
107
+ **Ошибка:** присутствует поле `error`:
108
+
109
+ ```jsonc
110
+ { "error": { "code": 401, "message": "Unauthorized", "httpStatus": 401, "details": "…" } }
111
+ ```
112
+
113
+ Обрабатывай отдельно: `401` → на экран логина; `403` → «нет прав»; остальное → текст из `details`.
114
+
115
+ ---
116
+
117
+ ## 4. Справочник эндпоинтов
118
+
119
+ | Эндпоинт | Роль | Параметры | Назначение |
120
+ |---|---|---|---|
121
+ | `login` | — | `login`, `password`, `totp?` | вход, выдача сессии |
122
+ | `enroll2fa` | — | `enrollmentToken`, `totp` | подключение 2FA |
123
+ | `logout` | любая | — | отозвать сессию |
124
+ | `procinfo` | dashboard | — | снимок процесса: память, event-loop, fd, pm2, сокеты |
125
+ | `fdreport` | dashboard | — | утечка fd/сокетов: срез + история |
126
+ | `httpstats` | dashboard | — | исходящие HTTP по хостам: p50/p95/p99 |
127
+ | `sysinfo` | dashboard | — | CPU, RAM, диски, сеть, load |
128
+ | `metrics` | dashboard | `range` | история: `hour`/`day`/`week` |
129
+ | `metricsWeeks` / `metricsWeek` | dashboard | `weekKey` | архивы с диска |
130
+ | `connections` | dashboard | — | активные исходящие соединения |
131
+ | `inventory` | dashboard | — | живые сервисы и эндпоинты |
132
+ | `db` | dashboard | `range` | healthcheck БД |
133
+ | `log` | logs | `lastID?` | логи приложения |
134
+ | `netlog` / `outgoingnetlog` | logs | — | входящие / исходящие запросы |
135
+ | `logfiles` | logs | — | какие файловые логи доступны |
136
+ | `logtail` | logs | `source`, `lines?`, `since?`, `filter?` | хвост файлового лога |
137
+ | `changeLogLevel` | admin | `level` | сменить уровень логирования |
138
+ | `restart` | admin | `confirm`, `password`, `totp?` | перезапуск процесса |
139
+
140
+ ---
141
+
142
+ ## 5. Перезапуск процесса — `restart`
143
+
144
+ Требует **повторного ввода учётных данных**, даже при живой сессии: это защищает от использования незапертого браузера.
145
+
146
+ ```js
147
+ const p = await sm("procinfo", {}, token);
148
+ if (!p.identity.pm2.managed) { /* не под pm2 — кнопку скрыть */ }
149
+
150
+ await sm("restart", {
151
+ confirm: p.identity.pm2.name, // точное имя процесса в pm2, из procinfo
152
+ password, // спросить заново в модальном окне
153
+ totp, // и код из приложения
154
+ }, token);
155
+ // → { restarting:true, name, pmId, by, graceMs }
156
+ ```
157
+
158
+ После `restarting:true` покажи «Restarting…», через 3–5 с опрашивай `procinfo`: запросы будут падать, пока процесс поднимается — это ожидаемо. Ответил и `uptimeSec` мал → «Restarted ✓».
159
+
160
+ ---
161
+
162
+ ## 6. Данные: что и как рисовать
163
+
164
+ ### `procinfo`
165
+
166
+ ```jsonc
167
+ { "identity": { "pid":1234, "uptimeSec":84213,
168
+ "pm2": { "managed":true, "name":"gateway-blue", "pmId":"3" } },
169
+ "memory": { "rss":178257920, "heapUsed":74883072, "heapTotal":106086400,
170
+ "external":21430272, "heap": { "sizeLimit":2197815296 } },
171
+ "eventLoop": { "p50Ms":0.9, "p90Ms":1.1, "p99Ms":1.16, "utilizationPct":3.2 },
172
+ "fileDescriptors": { "open":214, "limitSoft":1024, "limitHard":524288 },
173
+ "sockets": { "total":130, "byState": {"ESTABLISHED":40,"CLOSE_WAIT":78}, "topRemotes":[] } }
174
+ ```
175
+
176
+ `eventLoop.p99Ms` больше ~50 мс — петля тормозит. `sockets` равен `null` вне Linux.
177
+
178
+ ### `fdreport` — охота за утечкой соединений
179
+
180
+ ```jsonc
181
+ { "now": { "fdOpen":214, "limitSoft":1024,
182
+ "sockets": { "byState": {"CLOSE_WAIT":78}, "topRemotes":[{"host":"17.32.5.7","established":35}] } },
183
+ "sampleEveryMs": 600000,
184
+ "history": [ { "ts":…, "fdOpen":210, "fdSoftLimit":1024, "socketTotal":125,
185
+ "byState":{…}, "topRemote":{…}, "rss":187654144 } ] }
186
+ ```
187
+
188
+ - gauge `fdOpen / limitSoft`: жёлтый от 0.7, красный от 0.9 (`limitSoft === -1` → unlimited, процент не показывать);
189
+ - графики по `history`: `fdOpen` с порогом, `socketTotal`, `byState`;
190
+ - **`CLOSE_WAIT` монотонно растёт** → приложение не закрывает соединения; один хост растёт в `topRemotes` → зависшие исходящие к нему.
191
+
192
+ ### `httpstats` — p95 по партнёрам
193
+
194
+ ```jsonc
195
+ { "perHost": [ { "host":"https://apple-pay-gateway.apple.com:443", "count":1234,
196
+ "errors":12, "timeouts":3, "avgMs":210,
197
+ "p50Ms":180, "p95Ms":1431, "p99Ms":1750, "inFlight":2 } ],
198
+ "topSlow": [ /* топ-20 медленных */ ] }
199
+ ```
200
+
201
+ Веди график `p95Ms` по каждому хосту: ползущий вверх p95 — ранний признак деградации, ведущей к таймаутам.
202
+
203
+ ### `metrics` — минутные слоты
204
+
205
+ ```jsonc
206
+ [ { "timestamp":1789107000000, "slot": {
207
+ "yyyymmddhhii":202609110910, "cpu":0.26,
208
+ "memory": { "rss":170, "heapUsed":75, "heapTotal":90, "external":20 }, // МБ
209
+ "successEp": {"/api/session/check/:ssid":41}, "failEp": {"/api/apple/pay/:ssid":1},
210
+ "http": {"https://…/startSession":10},
211
+ "http_ok": {"apple-pay-gateway.apple.com:443":9},
212
+ "http_fail": {"apple-pay-gateway.apple.com:443":1},
213
+ "httpP95": {"apple-pay-gateway.apple.com:443":1431},
214
+ "success_sql": {"INSERT INTO …":51}, "fail_sql": {} } } ]
215
+ ```
216
+
217
+ `http_ok`/`http_fail`/`httpP95` ключуются по **хосту**, `http` — по url-пути. При укрупнении бакетов счётчики суммируются, `httpP95` берёт **max** (перцентиль усреднять нельзя).
218
+
219
+ **Долгая история:** `metricsWeeks` → список недель, `metricsWeek {weekKey}` → неделя с диска (живая поминутно, архив почасово). Требует `metricsDir` на бэке.
220
+
221
+ ### Четыре коррелированных графика
222
+
223
+ Все едут из **одного** запроса `metrics`, общая ось X = `timestamp`:
224
+
225
+ | # | График | Поле | Как считать |
226
+ |---|---|---|---|
227
+ | 1 | Входящие OK/FAIL (по endpoint) | `successEp`, `failEp` | сумма значений; без фильтра — total |
228
+ | 2 | Исходящие OK/FAIL (по host) | `http_ok`, `http_fail` | то же; список хостов — объединение ключей |
229
+ | 3 | Память | `memory` | `rss` и `external` (сигнал утечки) |
230
+ | 4 | p95 по host | `httpP95` | нет ключа → **разрыв линии, не ноль** |
231
+
232
+ Для «все хосты» на графике 4 бери **max**, не сумму.
233
+
234
+ > Слоты до апгрейда не содержат `http_ok`/`http_fail`/`httpP95` — трактуй пустое как «нет данных».
235
+
236
+ ### `logfiles` / `logtail` — файловые логи
237
+
238
+ Источники: `process-out`, `process-err` (логи pm2, определяются автоматически), `nginx-access`, `nginx-error` (пути задаются на бэке). Фронт выбирает только **идентификатор**, путь передать нельзя.
239
+
240
+ ```js
241
+ let r = await sm("logtail", { source:"process-out", lines:200 }, token);
242
+ r = await sm("logtail", { source:"process-out", since:r.cursor }, token); // только новое
243
+ ```
244
+
245
+ ```jsonc
246
+ { "source":"process-out", "path":"…", "size":2068890, "cursor":2068890,
247
+ "rotated":false, "truncated":false, "lines":["INFO server started", …] }
248
+ ```
249
+
250
+ - `rotated: true` → файл ротирован, очисти экран;
251
+ - `truncated: true` → пропущена середина, покажи разделитель;
252
+ - строки **уже замаскированы**: карты, CVV, пароли, токены, JWT. Фильтр работает по замаскированному тексту, поэтому искать по номеру карты нельзя — это намеренно;
253
+ - недописанная строка придёт целиком в следующем ответе; за раз читается не больше 512 КБ.
254
+
255
+ ---
256
+
257
+ ## 7. Частота опроса
258
+
259
+ | Данные | Частота |
260
+ |---|---|
261
+ | `procinfo`, `connections`, `httpstats`, `metrics`, `sysinfo` | 30–60 с |
262
+ | `fdreport` | 30–60 с (история обновляется раз в 10 мин) |
263
+ | `logtail` с `since` | 2–5 с, пока вкладка лога открыта |
264
+ | `metricsWeek`, `logfiles` | по действию пользователя |
265
+
266
+ Графики должны переживать пустые массивы и `null`; поля, доступные только на Linux (`sockets`), могут отсутствовать.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badmfck-api-server",
3
- "version": "5.0.8",
3
+ "version": "5.0.9",
4
4
  "description": "Simple API http server based on express",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",