@flowerforce/flowerbase 1.6.2 → 1.6.3-beta.1
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.
- package/README.md +49 -87
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +15 -3
- package/dist/auth/providers/anon-user/controller.d.ts.map +1 -1
- package/dist/auth/providers/anon-user/controller.js +5 -1
- package/dist/auth/providers/custom-function/schema.d.ts +1 -0
- package/dist/auth/providers/custom-function/schema.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/schema.js +1 -0
- package/dist/auth/utils.d.ts +7 -0
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +6 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +26 -5
- package/dist/features/endpoints/utils.d.ts.map +1 -1
- package/dist/features/endpoints/utils.js +18 -0
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/monitoring/plugin.d.ts +7 -0
- package/dist/monitoring/plugin.d.ts.map +1 -0
- package/dist/monitoring/plugin.js +1319 -0
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +10 -2
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +10 -1
- package/package.json +5 -3
- package/src/auth/controller.ts +15 -1
- package/src/auth/providers/anon-user/controller.ts +5 -0
- package/src/auth/providers/custom-function/schema.ts +23 -24
- package/src/auth/utils.ts +6 -0
- package/src/constants.ts +22 -0
- package/src/features/endpoints/utils.ts +18 -0
- package/src/features/functions/controller.ts +10 -2
- package/src/index.ts +10 -1
- package/src/monitoring/plugin.ts +1501 -0
- package/src/monitoring/ui.css +1049 -0
- package/src/monitoring/ui.html +293 -0
- package/src/monitoring/ui.js +1931 -0
- package/src/utils/initializer/exposeRoutes.ts +10 -2
- package/src/utils/initializer/registerPlugins.ts +13 -2
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
16
|
+
const websocket_1 = __importDefault(require("@fastify/websocket"));
|
|
17
|
+
require("@fastify/websocket");
|
|
18
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const mongodb_1 = require("mongodb");
|
|
21
|
+
const constants_1 = require("../constants");
|
|
22
|
+
const services_1 = require("../services");
|
|
23
|
+
const state_1 = require("../state");
|
|
24
|
+
const handleUserRegistration_1 = __importDefault(require("../shared/handleUserRegistration"));
|
|
25
|
+
const handleUserRegistration_model_1 = require("../shared/models/handleUserRegistration.model");
|
|
26
|
+
const crypto_1 = require("../utils/crypto");
|
|
27
|
+
const context_1 = require("../utils/context");
|
|
28
|
+
const utils_1 = require("../services/mongodb-atlas/utils");
|
|
29
|
+
const utils_2 = require("../utils/roles/machines/utils");
|
|
30
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
31
|
+
const MAX_DEPTH = 15;
|
|
32
|
+
const MAX_ARRAY = 50;
|
|
33
|
+
const MAX_STRING = 500;
|
|
34
|
+
const COLLECTION_PAGE_SIZE = 50;
|
|
35
|
+
const MONIT_REALM = 'Flowerbase Monitor';
|
|
36
|
+
const isTestEnv = () => process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
|
|
37
|
+
const isPromiseLike = (value) => !!value && typeof value === 'object' && typeof value.then === 'function';
|
|
38
|
+
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
39
|
+
const safeStringify = (value) => {
|
|
40
|
+
const seen = new WeakSet();
|
|
41
|
+
try {
|
|
42
|
+
return JSON.stringify(value, (key, val) => {
|
|
43
|
+
if (typeof val === 'bigint')
|
|
44
|
+
return val.toString();
|
|
45
|
+
if (val && typeof val === 'object') {
|
|
46
|
+
if (seen.has(val))
|
|
47
|
+
return '[Circular]';
|
|
48
|
+
seen.add(val);
|
|
49
|
+
}
|
|
50
|
+
return val;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (_a) {
|
|
54
|
+
return String(value);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const SENSITIVE_KEYS = [
|
|
58
|
+
/pass(word)?/i,
|
|
59
|
+
/secret/i,
|
|
60
|
+
/token/i,
|
|
61
|
+
/authorization/i,
|
|
62
|
+
/cookie/i,
|
|
63
|
+
/api[-_]?key/i,
|
|
64
|
+
/access[-_]?key/i,
|
|
65
|
+
/refresh[-_]?token/i,
|
|
66
|
+
/signature/i,
|
|
67
|
+
/private/i
|
|
68
|
+
];
|
|
69
|
+
const redactString = (value) => {
|
|
70
|
+
let result = value;
|
|
71
|
+
result = result.replace(/(Bearer|Basic)\s+[A-Za-z0-9._+\\/=-]+/gi, '$1 [redacted]');
|
|
72
|
+
result = result.replace(/(password|pass|pwd|secret|token|api[_-]?key|access[_-]?key|refresh[_-]?token)\s*[=:]\s*[^\\s,;]+/gi, '$1=[redacted]');
|
|
73
|
+
if (result.length > MAX_STRING) {
|
|
74
|
+
result = result.slice(0, MAX_STRING) + '...[truncated]';
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
};
|
|
78
|
+
const stripSensitiveFields = (value) => {
|
|
79
|
+
const out = {};
|
|
80
|
+
Object.keys(value).forEach((key) => {
|
|
81
|
+
if (SENSITIVE_KEYS.some((re) => re.test(key)))
|
|
82
|
+
return;
|
|
83
|
+
out[key] = value[key];
|
|
84
|
+
});
|
|
85
|
+
return out;
|
|
86
|
+
};
|
|
87
|
+
const isErrorLike = (value) => {
|
|
88
|
+
if (!value || typeof value !== 'object')
|
|
89
|
+
return false;
|
|
90
|
+
if (value instanceof Error)
|
|
91
|
+
return true;
|
|
92
|
+
const record = value;
|
|
93
|
+
return (typeof record.message === 'string' ||
|
|
94
|
+
typeof record.stack === 'string' ||
|
|
95
|
+
typeof record.name === 'string');
|
|
96
|
+
};
|
|
97
|
+
const sanitizeErrorLike = (value, depth) => {
|
|
98
|
+
const out = {};
|
|
99
|
+
const names = new Set(Object.getOwnPropertyNames(value));
|
|
100
|
+
['name', 'message', 'stack', 'code', 'statusCode', 'cause'].forEach((key) => names.add(key));
|
|
101
|
+
names.forEach((key) => {
|
|
102
|
+
if (SENSITIVE_KEYS.some((re) => re.test(key))) {
|
|
103
|
+
out[key] = '[redacted]';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const raw = value[key];
|
|
107
|
+
if (raw === value) {
|
|
108
|
+
out[key] = '[Circular]';
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if ((key === 'message' || key === 'stack') && typeof raw === 'string') {
|
|
112
|
+
out[key] = constants_1.DEFAULT_CONFIG.MONIT_REDACT_ERROR_DETAILS ? redactString(raw) : raw;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (typeof raw === 'string') {
|
|
116
|
+
out[key] = redactString(raw);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (raw !== undefined) {
|
|
120
|
+
out[key] = sanitize(raw, depth + 1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
if (value instanceof Error) {
|
|
124
|
+
if (!out.name)
|
|
125
|
+
out.name = value.name;
|
|
126
|
+
if (!out.message) {
|
|
127
|
+
out.message = constants_1.DEFAULT_CONFIG.MONIT_REDACT_ERROR_DETAILS ? redactString(value.message) : value.message;
|
|
128
|
+
}
|
|
129
|
+
if (!out.stack && value.stack) {
|
|
130
|
+
out.stack = constants_1.DEFAULT_CONFIG.MONIT_REDACT_ERROR_DETAILS ? redactString(value.stack) : value.stack;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
const getErrorDetails = (error) => {
|
|
136
|
+
if (isErrorLike(error)) {
|
|
137
|
+
const sanitized = sanitizeErrorLike(error, 0);
|
|
138
|
+
const message = typeof sanitized.message === 'string' && sanitized.message
|
|
139
|
+
? sanitized.message
|
|
140
|
+
: typeof sanitized.name === 'string' && sanitized.name
|
|
141
|
+
? sanitized.name
|
|
142
|
+
: safeStringify(error);
|
|
143
|
+
const stack = typeof sanitized.stack === 'string' ? sanitized.stack : undefined;
|
|
144
|
+
return { message, stack };
|
|
145
|
+
}
|
|
146
|
+
if (typeof error === 'string')
|
|
147
|
+
return { message: error };
|
|
148
|
+
return { message: safeStringify(error) };
|
|
149
|
+
};
|
|
150
|
+
const sanitize = (value, depth = 0) => {
|
|
151
|
+
if (depth > MAX_DEPTH)
|
|
152
|
+
return '[max-depth]';
|
|
153
|
+
if (value === null || value === undefined)
|
|
154
|
+
return value;
|
|
155
|
+
if (value instanceof Date)
|
|
156
|
+
return value.toISOString();
|
|
157
|
+
if (Buffer.isBuffer(value))
|
|
158
|
+
return `[buffer ${value.length} bytes]`;
|
|
159
|
+
if (isErrorLike(value)) {
|
|
160
|
+
return sanitizeErrorLike(value, depth);
|
|
161
|
+
}
|
|
162
|
+
if (typeof value === 'object') {
|
|
163
|
+
const maybeObjectId = value;
|
|
164
|
+
if ((maybeObjectId === null || maybeObjectId === void 0 ? void 0 : maybeObjectId._bsontype) === 'ObjectId' && typeof maybeObjectId.toString === 'function') {
|
|
165
|
+
return maybeObjectId.toString();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (typeof value === 'string')
|
|
169
|
+
return redactString(value);
|
|
170
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
171
|
+
return value;
|
|
172
|
+
if (typeof value === 'bigint')
|
|
173
|
+
return value.toString();
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
const items = value.slice(0, MAX_ARRAY).map((item) => sanitize(item, depth + 1));
|
|
176
|
+
if (value.length > MAX_ARRAY) {
|
|
177
|
+
items.push(`[+${value.length - MAX_ARRAY} items]`);
|
|
178
|
+
}
|
|
179
|
+
return items;
|
|
180
|
+
}
|
|
181
|
+
if (typeof value === 'object') {
|
|
182
|
+
const obj = value;
|
|
183
|
+
const out = {};
|
|
184
|
+
Object.keys(obj).forEach((key) => {
|
|
185
|
+
if (SENSITIVE_KEYS.some((re) => re.test(key))) {
|
|
186
|
+
out[key] = '[redacted]';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
out[key] = sanitize(obj[key], depth + 1);
|
|
190
|
+
});
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
const pickHeaders = (headers) => {
|
|
196
|
+
const keys = ['user-agent', 'content-type', 'x-forwarded-for', 'host', 'origin', 'referer'];
|
|
197
|
+
const picked = {};
|
|
198
|
+
keys.forEach((key) => {
|
|
199
|
+
const value = headers[key];
|
|
200
|
+
if (value)
|
|
201
|
+
picked[key] = value;
|
|
202
|
+
});
|
|
203
|
+
return sanitize(picked);
|
|
204
|
+
};
|
|
205
|
+
const createEventStore = (maxAgeMs, maxEvents) => {
|
|
206
|
+
const events = [];
|
|
207
|
+
const trim = () => {
|
|
208
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
209
|
+
while (events.length && events[0].ts < cutoff) {
|
|
210
|
+
events.shift();
|
|
211
|
+
}
|
|
212
|
+
if (events.length > maxEvents) {
|
|
213
|
+
events.splice(0, events.length - maxEvents);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
add(event) {
|
|
218
|
+
events.push(event);
|
|
219
|
+
trim();
|
|
220
|
+
},
|
|
221
|
+
list(query) {
|
|
222
|
+
trim();
|
|
223
|
+
let result = events.slice();
|
|
224
|
+
if (query === null || query === void 0 ? void 0 : query.type) {
|
|
225
|
+
result = result.filter((event) => event.type === query.type);
|
|
226
|
+
}
|
|
227
|
+
if (query === null || query === void 0 ? void 0 : query.q) {
|
|
228
|
+
const q = query.q.toLowerCase();
|
|
229
|
+
result = result.filter((event) => safeStringify(event).toLowerCase().includes(q));
|
|
230
|
+
}
|
|
231
|
+
if ((query === null || query === void 0 ? void 0 : query.limit) && query.limit > 0) {
|
|
232
|
+
result = result.slice(-query.limit);
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
},
|
|
236
|
+
clear() {
|
|
237
|
+
events.length = 0;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
const classifyRequest = (url) => {
|
|
242
|
+
if (url.includes('/auth') || url.includes('/login') || url.includes('/register') || url.includes('/logout')) {
|
|
243
|
+
return 'auth';
|
|
244
|
+
}
|
|
245
|
+
if (url.includes('/functions'))
|
|
246
|
+
return 'function';
|
|
247
|
+
if (url.includes('/endpoint/'))
|
|
248
|
+
return 'http_endpoint';
|
|
249
|
+
if (url.includes('/triggers'))
|
|
250
|
+
return 'trigger';
|
|
251
|
+
if (url.includes('/api/'))
|
|
252
|
+
return 'api';
|
|
253
|
+
return 'http';
|
|
254
|
+
};
|
|
255
|
+
const createEventId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
256
|
+
const buildRulesMeta = (meta) => {
|
|
257
|
+
var _a, _b, _c;
|
|
258
|
+
if (!meta || meta.serviceName !== 'mongodb-atlas' || !meta.collection || !meta.rules) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const collectionRules = meta.rules[meta.collection];
|
|
262
|
+
if (!collectionRules) {
|
|
263
|
+
return { collection: meta.collection, roles: [], filters: [] };
|
|
264
|
+
}
|
|
265
|
+
const filters = (_a = collectionRules.filters) !== null && _a !== void 0 ? _a : [];
|
|
266
|
+
const roles = (_b = collectionRules.roles) !== null && _b !== void 0 ? _b : [];
|
|
267
|
+
const user = ((_c = meta.user) !== null && _c !== void 0 ? _c : {});
|
|
268
|
+
const matchedFilters = (0, utils_1.getValidRule)({ filters, user });
|
|
269
|
+
const matchedFilterNames = matchedFilters.map((filter) => filter.name);
|
|
270
|
+
const matchedFilterQueries = matchedFilters.map((filter) => filter.query);
|
|
271
|
+
const roleCandidates = roles
|
|
272
|
+
.filter((role) => (0, utils_2.checkApplyWhen)(role.apply_when, user, null))
|
|
273
|
+
.map((role) => role.name);
|
|
274
|
+
return {
|
|
275
|
+
collection: meta.collection,
|
|
276
|
+
roles: roles.map((role) => role.name),
|
|
277
|
+
roleCandidates,
|
|
278
|
+
filters: filters.map((filter) => filter.name),
|
|
279
|
+
matchedFilters: matchedFilterNames,
|
|
280
|
+
matchedFilterQueries,
|
|
281
|
+
runAsSystem: !!meta.runAsSystem
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const buildCollectionRulesSnapshot = (rules, collection, user, runAsSystem) => {
|
|
285
|
+
const collectionRules = rules === null || rules === void 0 ? void 0 : rules[collection];
|
|
286
|
+
return collectionRules !== null && collectionRules !== void 0 ? collectionRules : null;
|
|
287
|
+
};
|
|
288
|
+
const resolveAssetCandidates = (filename, prefix) => {
|
|
289
|
+
const rootDir = process.cwd();
|
|
290
|
+
const cleanPrefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
291
|
+
const candidates = [];
|
|
292
|
+
const addCandidate = (candidate) => {
|
|
293
|
+
if (!candidates.includes(candidate))
|
|
294
|
+
candidates.push(candidate);
|
|
295
|
+
};
|
|
296
|
+
if (cleanPrefix) {
|
|
297
|
+
addCandidate(node_path_1.default.join(rootDir, cleanPrefix, filename));
|
|
298
|
+
}
|
|
299
|
+
addCandidate(node_path_1.default.join(rootDir, 'monitoring', filename));
|
|
300
|
+
// Fallbacks: try package-local assets (works in dev and when bundled).
|
|
301
|
+
addCandidate(node_path_1.default.join(__dirname, filename));
|
|
302
|
+
addCandidate(node_path_1.default.join(__dirname, '..', '..', 'src', 'monitoring', filename));
|
|
303
|
+
return candidates;
|
|
304
|
+
};
|
|
305
|
+
const resolveUserContext = (app, userId, userPayload) => __awaiter(void 0, void 0, void 0, function* () {
|
|
306
|
+
var _a, _b;
|
|
307
|
+
if (userPayload && typeof userPayload === 'object') {
|
|
308
|
+
return stripSensitiveFields(userPayload);
|
|
309
|
+
}
|
|
310
|
+
if (!userId)
|
|
311
|
+
return undefined;
|
|
312
|
+
const normalizedUserId = userId.trim();
|
|
313
|
+
const db = app.mongo.client.db(constants_1.DB_NAME);
|
|
314
|
+
const authCollection = (_a = constants_1.AUTH_CONFIG.authCollection) !== null && _a !== void 0 ? _a : 'auth_users';
|
|
315
|
+
const userCollection = constants_1.AUTH_CONFIG.userCollection;
|
|
316
|
+
const userIdField = (_b = constants_1.AUTH_CONFIG.user_id_field) !== null && _b !== void 0 ? _b : 'id';
|
|
317
|
+
const isObjectId = mongodb_1.ObjectId.isValid(normalizedUserId);
|
|
318
|
+
const authSelector = isObjectId
|
|
319
|
+
? { _id: new mongodb_1.ObjectId(normalizedUserId) }
|
|
320
|
+
: { id: normalizedUserId };
|
|
321
|
+
const authUser = yield db.collection(authCollection).findOne(authSelector);
|
|
322
|
+
let customUser = null;
|
|
323
|
+
if (userCollection) {
|
|
324
|
+
const customSelector = { [userIdField]: normalizedUserId };
|
|
325
|
+
customUser = yield db.collection(userCollection).findOne(customSelector);
|
|
326
|
+
if (!customUser && isObjectId) {
|
|
327
|
+
customUser = yield db.collection(userCollection).findOne({ _id: new mongodb_1.ObjectId(normalizedUserId) });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const id = authUser && typeof authUser._id !== 'undefined'
|
|
331
|
+
? String(authUser._id)
|
|
332
|
+
: (customUser && typeof customUser[userIdField] !== 'undefined'
|
|
333
|
+
? String(customUser[userIdField])
|
|
334
|
+
: normalizedUserId);
|
|
335
|
+
const user_data = Object.assign(Object.assign({}, (customUser ? stripSensitiveFields(customUser) : {})), { id, _id: id, email: authUser && typeof authUser.email === 'string'
|
|
336
|
+
? authUser.email
|
|
337
|
+
: undefined });
|
|
338
|
+
const user = {
|
|
339
|
+
id,
|
|
340
|
+
user_data,
|
|
341
|
+
data: user_data,
|
|
342
|
+
custom_data: user_data
|
|
343
|
+
};
|
|
344
|
+
if (isObjectId) {
|
|
345
|
+
user._id = new mongodb_1.ObjectId(id);
|
|
346
|
+
}
|
|
347
|
+
return user;
|
|
348
|
+
});
|
|
349
|
+
const getUserInfo = (resolvedUser) => {
|
|
350
|
+
var _a;
|
|
351
|
+
if (!resolvedUser || typeof resolvedUser !== 'object')
|
|
352
|
+
return undefined;
|
|
353
|
+
const record = resolvedUser;
|
|
354
|
+
const id = typeof record.id === 'string' ? record.id : undefined;
|
|
355
|
+
const email = typeof record.email === 'string'
|
|
356
|
+
? record.email
|
|
357
|
+
: (typeof ((_a = record.user_data) === null || _a === void 0 ? void 0 : _a.email) === 'string' ? record.user_data.email : undefined);
|
|
358
|
+
if (!id && !email)
|
|
359
|
+
return undefined;
|
|
360
|
+
return { id, email };
|
|
361
|
+
};
|
|
362
|
+
const readAsset = (filename, prefix) => {
|
|
363
|
+
const candidates = resolveAssetCandidates(filename, prefix);
|
|
364
|
+
for (const candidate of candidates) {
|
|
365
|
+
try {
|
|
366
|
+
if (node_fs_1.default.existsSync(candidate)) {
|
|
367
|
+
return node_fs_1.default.readFileSync(candidate, 'utf8');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (_a) {
|
|
371
|
+
// ignore and try next
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return '';
|
|
375
|
+
};
|
|
376
|
+
const wrapServicesForMonitoring = (addEvent) => {
|
|
377
|
+
const wrapped = services_1.services;
|
|
378
|
+
if (wrapped.__monitWrapped)
|
|
379
|
+
return;
|
|
380
|
+
wrapped.__monitWrapped = true;
|
|
381
|
+
const serviceTypeMap = {
|
|
382
|
+
api: 'api',
|
|
383
|
+
aws: 'aws',
|
|
384
|
+
auth: 'auth',
|
|
385
|
+
'mongodb-atlas': 'mongo'
|
|
386
|
+
};
|
|
387
|
+
const initMethodMap = {
|
|
388
|
+
aws: new Set(['lambda', 's3']),
|
|
389
|
+
'mongodb-atlas': new Set(['db', 'collection', 'limit', 'skip', 'toArray'])
|
|
390
|
+
};
|
|
391
|
+
const cache = new WeakMap();
|
|
392
|
+
const wrapValue = (value, path, serviceName, meta) => {
|
|
393
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function'))
|
|
394
|
+
return value;
|
|
395
|
+
if (isPromiseLike(value))
|
|
396
|
+
return value;
|
|
397
|
+
if (cache.has(value)) {
|
|
398
|
+
return cache.get(value);
|
|
399
|
+
}
|
|
400
|
+
const handler = {
|
|
401
|
+
get(target, prop, receiver) {
|
|
402
|
+
const propValue = Reflect.get(target, prop, receiver);
|
|
403
|
+
if (typeof prop === 'symbol')
|
|
404
|
+
return propValue;
|
|
405
|
+
if (prop === 'constructor' || prop === 'toJSON')
|
|
406
|
+
return propValue;
|
|
407
|
+
if (typeof propValue === 'function') {
|
|
408
|
+
const propName = String(prop);
|
|
409
|
+
const fnPath = `${path}.${propName}`;
|
|
410
|
+
const wrappedFn = (...args) => {
|
|
411
|
+
var _a, _b;
|
|
412
|
+
let nextMeta = meta;
|
|
413
|
+
if (serviceName === 'mongodb-atlas') {
|
|
414
|
+
if (propName === 'db' && typeof args[0] === 'string') {
|
|
415
|
+
nextMeta = Object.assign(Object.assign({}, (meta !== null && meta !== void 0 ? meta : { serviceName })), { dbName: args[0], serviceName });
|
|
416
|
+
}
|
|
417
|
+
if (propName === 'collection' && typeof args[0] === 'string') {
|
|
418
|
+
nextMeta = Object.assign(Object.assign({}, (meta !== null && meta !== void 0 ? meta : { serviceName })), { collection: args[0], serviceName });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const shouldLog = !((_a = initMethodMap[serviceName]) === null || _a === void 0 ? void 0 : _a.has(propName));
|
|
422
|
+
if (shouldLog) {
|
|
423
|
+
const ruleInfo = buildRulesMeta(nextMeta !== null && nextMeta !== void 0 ? nextMeta : meta);
|
|
424
|
+
addEvent({
|
|
425
|
+
id: createEventId(),
|
|
426
|
+
ts: Date.now(),
|
|
427
|
+
type: (_b = serviceTypeMap[serviceName]) !== null && _b !== void 0 ? _b : 'service',
|
|
428
|
+
source: `service:${serviceName}`,
|
|
429
|
+
message: fnPath,
|
|
430
|
+
data: sanitize({ args, rules: ruleInfo })
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
let result;
|
|
434
|
+
try {
|
|
435
|
+
result = propValue.apply(target, args);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
addEvent({
|
|
439
|
+
id: createEventId(),
|
|
440
|
+
ts: Date.now(),
|
|
441
|
+
type: 'error',
|
|
442
|
+
source: `service:${serviceName}`,
|
|
443
|
+
message: `error ${fnPath}`,
|
|
444
|
+
data: sanitize({ error })
|
|
445
|
+
});
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
if (isPromiseLike(result)) {
|
|
449
|
+
return result.catch((error) => {
|
|
450
|
+
addEvent({
|
|
451
|
+
id: createEventId(),
|
|
452
|
+
ts: Date.now(),
|
|
453
|
+
type: 'error',
|
|
454
|
+
source: `service:${serviceName}`,
|
|
455
|
+
message: `error ${fnPath}`,
|
|
456
|
+
data: sanitize({ error })
|
|
457
|
+
});
|
|
458
|
+
throw error;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return wrapValue(result, fnPath, serviceName, nextMeta);
|
|
462
|
+
};
|
|
463
|
+
return wrappedFn;
|
|
464
|
+
}
|
|
465
|
+
return wrapValue(propValue, `${path}.${String(prop)}`, serviceName, meta);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const proxied = new Proxy(value, handler);
|
|
469
|
+
cache.set(value, proxied);
|
|
470
|
+
return proxied;
|
|
471
|
+
};
|
|
472
|
+
Object.keys(services_1.services).forEach((serviceName) => {
|
|
473
|
+
const original = wrapped[serviceName];
|
|
474
|
+
wrapped[serviceName] = (app, options) => {
|
|
475
|
+
const instance = original(app, options);
|
|
476
|
+
const meta = {
|
|
477
|
+
serviceName,
|
|
478
|
+
rules: options.rules,
|
|
479
|
+
user: options.user,
|
|
480
|
+
runAsSystem: Boolean(options.run_as_system)
|
|
481
|
+
};
|
|
482
|
+
return wrapValue(instance, serviceName, serviceName, meta);
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
const createMonitoringPlugin = (0, fastify_plugin_1.default)((app_1, ...args_1) => __awaiter(void 0, [app_1, ...args_1], void 0, function* (app, opts = {}) {
|
|
487
|
+
if (isTestEnv())
|
|
488
|
+
return;
|
|
489
|
+
const enabled = constants_1.DEFAULT_CONFIG.MONIT_ENABLED;
|
|
490
|
+
if (!enabled)
|
|
491
|
+
return;
|
|
492
|
+
const rawPrefix = typeof opts.basePath === 'string' ? opts.basePath : '/monit';
|
|
493
|
+
const normalizedPrefix = rawPrefix.startsWith('/') ? rawPrefix : `/${rawPrefix}`;
|
|
494
|
+
const prefix = normalizedPrefix.endsWith('/')
|
|
495
|
+
? normalizedPrefix.slice(0, -1)
|
|
496
|
+
: normalizedPrefix;
|
|
497
|
+
const maxAgeMs = Math.max(1, constants_1.DEFAULT_CONFIG.MONIT_CACHE_HOURS) * 60 * 60 * 1000;
|
|
498
|
+
const maxEvents = Math.max(1000, constants_1.DEFAULT_CONFIG.MONIT_MAX_EVENTS);
|
|
499
|
+
const allowedIps = constants_1.DEFAULT_CONFIG.MONIT_ALLOWED_IPS;
|
|
500
|
+
const rateLimitWindowMs = Math.max(0, constants_1.DEFAULT_CONFIG.MONIT_RATE_LIMIT_WINDOW_MS);
|
|
501
|
+
const rateLimitMax = Math.max(0, constants_1.DEFAULT_CONFIG.MONIT_RATE_LIMIT_MAX);
|
|
502
|
+
const allowInvoke = constants_1.DEFAULT_CONFIG.MONIT_ALLOW_INVOKE;
|
|
503
|
+
const allowEdit = constants_1.DEFAULT_CONFIG.MONIT_ALLOW_EDIT;
|
|
504
|
+
const eventStore = createEventStore(maxAgeMs || DAY_MS, maxEvents);
|
|
505
|
+
const functionHistory = [];
|
|
506
|
+
const maxHistory = 30;
|
|
507
|
+
const collectionHistory = [];
|
|
508
|
+
const maxCollectionHistory = 30;
|
|
509
|
+
const statsState = {
|
|
510
|
+
lastCpu: process.cpuUsage(),
|
|
511
|
+
lastHr: process.hrtime.bigint(),
|
|
512
|
+
maxRssMb: 0,
|
|
513
|
+
maxCpu: 0
|
|
514
|
+
};
|
|
515
|
+
const clients = new Set();
|
|
516
|
+
const addFunctionHistory = (entry) => {
|
|
517
|
+
functionHistory.unshift(entry);
|
|
518
|
+
if (functionHistory.length > maxHistory) {
|
|
519
|
+
functionHistory.splice(maxHistory);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const addCollectionHistory = (entry) => {
|
|
523
|
+
collectionHistory.unshift(entry);
|
|
524
|
+
if (collectionHistory.length > maxCollectionHistory) {
|
|
525
|
+
collectionHistory.splice(maxCollectionHistory);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const rateBucket = new Map();
|
|
529
|
+
const isRateLimited = (key) => {
|
|
530
|
+
if (rateLimitMax <= 0 || rateLimitWindowMs <= 0)
|
|
531
|
+
return false;
|
|
532
|
+
const now = Date.now();
|
|
533
|
+
const current = rateBucket.get(key);
|
|
534
|
+
if (!current || now > current.resetAt) {
|
|
535
|
+
rateBucket.set(key, { count: 1, resetAt: now + rateLimitWindowMs });
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
current.count += 1;
|
|
539
|
+
if (current.count > rateLimitMax) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
};
|
|
544
|
+
const round1 = (value) => Math.round(value * 10) / 10;
|
|
545
|
+
const getStats = () => {
|
|
546
|
+
const mem = process.memoryUsage();
|
|
547
|
+
const rssMb = mem.rss / (1024 * 1024);
|
|
548
|
+
const now = process.hrtime.bigint();
|
|
549
|
+
const currentCpu = process.cpuUsage();
|
|
550
|
+
const deltaCpu = {
|
|
551
|
+
user: currentCpu.user - statsState.lastCpu.user,
|
|
552
|
+
system: currentCpu.system - statsState.lastCpu.system
|
|
553
|
+
};
|
|
554
|
+
const deltaTimeMicros = Number(now - statsState.lastHr) / 1000;
|
|
555
|
+
const cpuPercent = deltaTimeMicros > 0
|
|
556
|
+
? ((deltaCpu.user + deltaCpu.system) / deltaTimeMicros) * 100
|
|
557
|
+
: 0;
|
|
558
|
+
statsState.lastCpu = currentCpu;
|
|
559
|
+
statsState.lastHr = now;
|
|
560
|
+
statsState.maxRssMb = Math.max(statsState.maxRssMb, rssMb);
|
|
561
|
+
statsState.maxCpu = Math.max(statsState.maxCpu, cpuPercent);
|
|
562
|
+
return {
|
|
563
|
+
ramMb: round1(rssMb),
|
|
564
|
+
cpuPercent: round1(cpuPercent),
|
|
565
|
+
topRamMb: round1(statsState.maxRssMb),
|
|
566
|
+
topCpuPercent: round1(statsState.maxCpu),
|
|
567
|
+
uptimeSec: Math.round(process.uptime())
|
|
568
|
+
};
|
|
569
|
+
};
|
|
570
|
+
const addEvent = (event) => {
|
|
571
|
+
const sanitizedEvent = Object.assign(Object.assign({}, event), { data: sanitize(event.data), message: redactString(event.message) });
|
|
572
|
+
eventStore.add(sanitizedEvent);
|
|
573
|
+
const payload = JSON.stringify({ type: 'event', event: sanitizedEvent });
|
|
574
|
+
clients.forEach((client) => {
|
|
575
|
+
if (client.readyState !== 1)
|
|
576
|
+
return;
|
|
577
|
+
try {
|
|
578
|
+
client.send(payload);
|
|
579
|
+
}
|
|
580
|
+
catch (_a) {
|
|
581
|
+
clients.delete(client);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
wrapServicesForMonitoring(addEvent);
|
|
586
|
+
if (constants_1.DEFAULT_CONFIG.MONIT_CAPTURE_CONSOLE) {
|
|
587
|
+
const original = {
|
|
588
|
+
log: console.log,
|
|
589
|
+
error: console.error,
|
|
590
|
+
warn: console.warn
|
|
591
|
+
};
|
|
592
|
+
console.log = (...args) => {
|
|
593
|
+
addEvent({
|
|
594
|
+
id: createEventId(),
|
|
595
|
+
ts: Date.now(),
|
|
596
|
+
type: 'log',
|
|
597
|
+
source: 'console',
|
|
598
|
+
message: args.map((item) => (typeof item === 'string' ? item : safeStringify(item))).join(' '),
|
|
599
|
+
data: sanitize(args)
|
|
600
|
+
});
|
|
601
|
+
original.log(...args);
|
|
602
|
+
};
|
|
603
|
+
console.error = (...args) => {
|
|
604
|
+
addEvent({
|
|
605
|
+
id: createEventId(),
|
|
606
|
+
ts: Date.now(),
|
|
607
|
+
type: 'error',
|
|
608
|
+
source: 'console',
|
|
609
|
+
message: args.map((item) => (typeof item === 'string' ? item : safeStringify(item))).join(' '),
|
|
610
|
+
data: sanitize(args)
|
|
611
|
+
});
|
|
612
|
+
original.error(...args);
|
|
613
|
+
};
|
|
614
|
+
console.warn = (...args) => {
|
|
615
|
+
addEvent({
|
|
616
|
+
id: createEventId(),
|
|
617
|
+
ts: Date.now(),
|
|
618
|
+
type: 'warn',
|
|
619
|
+
source: 'console',
|
|
620
|
+
message: args.map((item) => (typeof item === 'string' ? item : safeStringify(item))).join(' '),
|
|
621
|
+
data: sanitize(args)
|
|
622
|
+
});
|
|
623
|
+
original.warn(...args);
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const hasCredentials = () => constants_1.DEFAULT_CONFIG.MONIT_USER && constants_1.DEFAULT_CONFIG.MONIT_PASSWORD;
|
|
627
|
+
const isAuthorized = (req) => {
|
|
628
|
+
if (!hasCredentials())
|
|
629
|
+
return false;
|
|
630
|
+
const header = req.headers.authorization;
|
|
631
|
+
if (!header || !header.startsWith('Basic '))
|
|
632
|
+
return false;
|
|
633
|
+
const encoded = header.slice('Basic '.length);
|
|
634
|
+
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
|
|
635
|
+
const [user, pass] = decoded.split(':');
|
|
636
|
+
return user === constants_1.DEFAULT_CONFIG.MONIT_USER && pass === constants_1.DEFAULT_CONFIG.MONIT_PASSWORD;
|
|
637
|
+
};
|
|
638
|
+
const isMonitRoute = (url) => {
|
|
639
|
+
const path = url.split('?')[0];
|
|
640
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
641
|
+
};
|
|
642
|
+
const shouldSkipLog = (req) => isMonitRoute(req.url);
|
|
643
|
+
app.addHook('onRequest', (req, reply, done) => {
|
|
644
|
+
if (isMonitRoute(req.url)) {
|
|
645
|
+
const audit = (status, reason) => {
|
|
646
|
+
addEvent({
|
|
647
|
+
id: createEventId(),
|
|
648
|
+
ts: Date.now(),
|
|
649
|
+
type: 'auth',
|
|
650
|
+
source: 'monit',
|
|
651
|
+
message: `monit ${status}`,
|
|
652
|
+
data: sanitize({
|
|
653
|
+
status,
|
|
654
|
+
reason,
|
|
655
|
+
ip: req.ip,
|
|
656
|
+
method: req.method,
|
|
657
|
+
path: req.url
|
|
658
|
+
})
|
|
659
|
+
});
|
|
660
|
+
};
|
|
661
|
+
const allowAllIps = allowedIps.includes('0.0.0.0') || allowedIps.includes('*');
|
|
662
|
+
if (allowedIps.length && !allowAllIps && !allowedIps.includes(req.ip)) {
|
|
663
|
+
audit('deny', 'ip');
|
|
664
|
+
reply.code(403).send({ message: 'Forbidden' });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (isRateLimited(req.ip)) {
|
|
668
|
+
audit('deny', 'rate_limit');
|
|
669
|
+
reply.code(429).send({ message: 'Too Many Requests' });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (!hasCredentials()) {
|
|
673
|
+
audit('deny', 'missing_credentials');
|
|
674
|
+
reply.code(503).send({ message: 'Monitoring credentials not configured' });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (!isAuthorized(req)) {
|
|
678
|
+
audit('deny', 'basic_auth');
|
|
679
|
+
reply
|
|
680
|
+
.code(401)
|
|
681
|
+
.header('WWW-Authenticate', `Basic realm="${MONIT_REALM}"`)
|
|
682
|
+
.send({ message: 'Unauthorized' });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
;
|
|
687
|
+
req.__monitStart = Date.now();
|
|
688
|
+
done();
|
|
689
|
+
});
|
|
690
|
+
app.addHook('onResponse', (req, reply, done) => {
|
|
691
|
+
var _a, _b, _c;
|
|
692
|
+
if (shouldSkipLog(req)) {
|
|
693
|
+
done();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const start = (_a = req.__monitStart) !== null && _a !== void 0 ? _a : Date.now();
|
|
697
|
+
const duration = Date.now() - start;
|
|
698
|
+
const url = (_b = req.url) !== null && _b !== void 0 ? _b : '';
|
|
699
|
+
const type = classifyRequest(url);
|
|
700
|
+
const data = {
|
|
701
|
+
method: req.method,
|
|
702
|
+
url,
|
|
703
|
+
statusCode: reply.statusCode,
|
|
704
|
+
durationMs: duration,
|
|
705
|
+
ip: req.ip,
|
|
706
|
+
query: sanitize(req.query),
|
|
707
|
+
headers: pickHeaders(req.headers)
|
|
708
|
+
};
|
|
709
|
+
if (type === 'function') {
|
|
710
|
+
const body = req.body;
|
|
711
|
+
if (body === null || body === void 0 ? void 0 : body.name) {
|
|
712
|
+
data.function = body.name;
|
|
713
|
+
data.arguments = sanitize(body.arguments);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (type === 'auth') {
|
|
717
|
+
const body = req.body;
|
|
718
|
+
if ((body === null || body === void 0 ? void 0 : body.email) || (body === null || body === void 0 ? void 0 : body.username)) {
|
|
719
|
+
data.user = (_c = body.email) !== null && _c !== void 0 ? _c : body.username;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
addEvent({
|
|
723
|
+
id: createEventId(),
|
|
724
|
+
ts: Date.now(),
|
|
725
|
+
type,
|
|
726
|
+
source: 'http',
|
|
727
|
+
message: `${req.method} ${url} -> ${reply.statusCode} (${duration}ms)`,
|
|
728
|
+
data
|
|
729
|
+
});
|
|
730
|
+
done();
|
|
731
|
+
});
|
|
732
|
+
app.addHook('onError', (req, reply, error, done) => {
|
|
733
|
+
var _a;
|
|
734
|
+
if (!shouldSkipLog(req)) {
|
|
735
|
+
addEvent({
|
|
736
|
+
id: createEventId(),
|
|
737
|
+
ts: Date.now(),
|
|
738
|
+
type: 'error',
|
|
739
|
+
source: 'http',
|
|
740
|
+
message: `${req.method} ${req.url} -> error`,
|
|
741
|
+
data: sanitize({ error: (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error })
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
done();
|
|
745
|
+
});
|
|
746
|
+
yield app.register(websocket_1.default);
|
|
747
|
+
const sendUi = (_req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
748
|
+
const raw = readAsset('ui.html', prefix);
|
|
749
|
+
if (!raw) {
|
|
750
|
+
const tried = resolveAssetCandidates('ui.html', prefix).join(', ');
|
|
751
|
+
reply.code(404).send(`ui.html not found. Tried: ${tried}`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const html = raw.replace(/__MONIT_BASE__/g, prefix);
|
|
755
|
+
reply.header('Cache-Control', 'no-store');
|
|
756
|
+
reply.type('text/html').send(html);
|
|
757
|
+
});
|
|
758
|
+
app.get(prefix, sendUi);
|
|
759
|
+
app.get(`${prefix}/`, sendUi);
|
|
760
|
+
app.get(`${prefix}/ui.css`, (_req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
761
|
+
const css = readAsset('ui.css', prefix);
|
|
762
|
+
if (!css) {
|
|
763
|
+
const tried = resolveAssetCandidates('ui.css', prefix).join(', ');
|
|
764
|
+
reply.code(404).send(`ui.css not found. Tried: ${tried}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
reply.header('Cache-Control', 'no-store');
|
|
768
|
+
reply.type('text/css').send(css);
|
|
769
|
+
}));
|
|
770
|
+
app.get(`${prefix}/ui.js`, (_req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
771
|
+
const raw = readAsset('ui.js', prefix);
|
|
772
|
+
if (!raw) {
|
|
773
|
+
const tried = resolveAssetCandidates('ui.js', prefix).join(', ');
|
|
774
|
+
reply.code(404).send(`ui.js not found. Tried: ${tried}`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const js = raw.replace(/__MONIT_BASE__/g, prefix);
|
|
778
|
+
reply.header('Cache-Control', 'no-store');
|
|
779
|
+
reply.type('application/javascript').send(js);
|
|
780
|
+
}));
|
|
781
|
+
app.get(`${prefix}/ws`, { websocket: true }, (connection) => {
|
|
782
|
+
var _a;
|
|
783
|
+
const socket = (_a = connection
|
|
784
|
+
.socket) !== null && _a !== void 0 ? _a : connection;
|
|
785
|
+
clients.add(socket);
|
|
786
|
+
socket.send(JSON.stringify({ type: 'init', events: eventStore.list({ limit: maxEvents }) }));
|
|
787
|
+
addEvent({
|
|
788
|
+
id: createEventId(),
|
|
789
|
+
ts: Date.now(),
|
|
790
|
+
type: 'log',
|
|
791
|
+
source: 'monit',
|
|
792
|
+
message: 'websocket connected'
|
|
793
|
+
});
|
|
794
|
+
socket.on('close', () => {
|
|
795
|
+
clients.delete(socket);
|
|
796
|
+
addEvent({
|
|
797
|
+
id: createEventId(),
|
|
798
|
+
ts: Date.now(),
|
|
799
|
+
type: 'log',
|
|
800
|
+
source: 'monit',
|
|
801
|
+
message: 'websocket disconnected'
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
app.get(`${prefix}/api/events`, (req) => __awaiter(void 0, void 0, void 0, function* () {
|
|
806
|
+
const query = req.query;
|
|
807
|
+
const limit = query.limit ? Number(query.limit) : undefined;
|
|
808
|
+
return {
|
|
809
|
+
items: eventStore.list({
|
|
810
|
+
q: query.q,
|
|
811
|
+
type: query.type,
|
|
812
|
+
limit
|
|
813
|
+
})
|
|
814
|
+
};
|
|
815
|
+
}));
|
|
816
|
+
app.get(`${prefix}/api/stats`, () => __awaiter(void 0, void 0, void 0, function* () { return getStats(); }));
|
|
817
|
+
app.get(`${prefix}/api/functions`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
818
|
+
const functionsList = state_1.StateManager.select('functions');
|
|
819
|
+
const items = Object.keys(functionsList || {}).map((name) => {
|
|
820
|
+
var _a, _b;
|
|
821
|
+
return ({
|
|
822
|
+
name,
|
|
823
|
+
private: !!((_a = functionsList[name]) === null || _a === void 0 ? void 0 : _a.private),
|
|
824
|
+
run_as_system: !!((_b = functionsList[name]) === null || _b === void 0 ? void 0 : _b.run_as_system)
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
return { items };
|
|
828
|
+
}));
|
|
829
|
+
app.get(`${prefix}/api/functions/:name`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
830
|
+
var _a;
|
|
831
|
+
if (!allowEdit) {
|
|
832
|
+
reply.code(403);
|
|
833
|
+
return { error: 'Function code access disabled' };
|
|
834
|
+
}
|
|
835
|
+
const params = req.params;
|
|
836
|
+
const name = params.name;
|
|
837
|
+
const functionsList = state_1.StateManager.select('functions');
|
|
838
|
+
const currentFunction = functionsList === null || functionsList === void 0 ? void 0 : functionsList[name];
|
|
839
|
+
if (!currentFunction) {
|
|
840
|
+
reply.code(404);
|
|
841
|
+
return { error: `Function "${name}" not found` };
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
name,
|
|
845
|
+
code: (_a = currentFunction.code) !== null && _a !== void 0 ? _a : '',
|
|
846
|
+
private: !!currentFunction.private,
|
|
847
|
+
run_as_system: !!currentFunction.run_as_system,
|
|
848
|
+
disable_arg_logs: !!currentFunction.disable_arg_logs
|
|
849
|
+
};
|
|
850
|
+
}));
|
|
851
|
+
app.get(`${prefix}/api/functions/history`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
852
|
+
return ({
|
|
853
|
+
items: functionHistory.slice(0, maxHistory)
|
|
854
|
+
});
|
|
855
|
+
}));
|
|
856
|
+
app.post(`${prefix}/api/functions/invoke`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
857
|
+
var _a, _b;
|
|
858
|
+
if (!allowInvoke) {
|
|
859
|
+
reply.code(403);
|
|
860
|
+
return { error: 'Function invocation disabled' };
|
|
861
|
+
}
|
|
862
|
+
const body = req.body;
|
|
863
|
+
const name = body === null || body === void 0 ? void 0 : body.name;
|
|
864
|
+
const args = Array.isArray(body === null || body === void 0 ? void 0 : body.arguments) ? body.arguments : [];
|
|
865
|
+
if (!name) {
|
|
866
|
+
reply.code(400);
|
|
867
|
+
return { error: 'Missing function name' };
|
|
868
|
+
}
|
|
869
|
+
const functionsList = state_1.StateManager.select('functions');
|
|
870
|
+
const rules = state_1.StateManager.select('rules');
|
|
871
|
+
const appRef = state_1.StateManager.select('app');
|
|
872
|
+
const services = state_1.StateManager.select('services');
|
|
873
|
+
const currentFunction = functionsList === null || functionsList === void 0 ? void 0 : functionsList[name];
|
|
874
|
+
if (!currentFunction) {
|
|
875
|
+
reply.code(404);
|
|
876
|
+
return { error: `Function "${name}" not found` };
|
|
877
|
+
}
|
|
878
|
+
if (!allowEdit && typeof (body === null || body === void 0 ? void 0 : body.code) === 'string' && body.code.trim()) {
|
|
879
|
+
reply.code(403);
|
|
880
|
+
return { error: 'Function override disabled' };
|
|
881
|
+
}
|
|
882
|
+
const overrideCode = typeof (body === null || body === void 0 ? void 0 : body.code) === 'string' && body.code.trim()
|
|
883
|
+
? body.code
|
|
884
|
+
: undefined;
|
|
885
|
+
const effectiveRunAsSystem = (body === null || body === void 0 ? void 0 : body.runAsSystem) !== false;
|
|
886
|
+
const effectiveFunction = overrideCode
|
|
887
|
+
? Object.assign(Object.assign({}, currentFunction), { code: overrideCode, run_as_system: effectiveRunAsSystem }) : Object.assign(Object.assign({}, currentFunction), { run_as_system: effectiveRunAsSystem });
|
|
888
|
+
const resolvedUser = yield resolveUserContext(app, body === null || body === void 0 ? void 0 : body.userId, body === null || body === void 0 ? void 0 : body.user);
|
|
889
|
+
const safeArgs = (Array.isArray(args) ? sanitize(args) : sanitize([args]));
|
|
890
|
+
const resolvedUserRecord = resolvedUser;
|
|
891
|
+
const userInfo = resolvedUserRecord
|
|
892
|
+
? {
|
|
893
|
+
id: typeof resolvedUserRecord.id === 'string' ? resolvedUserRecord.id : undefined,
|
|
894
|
+
email: typeof resolvedUserRecord.email === 'string'
|
|
895
|
+
? resolvedUserRecord.email
|
|
896
|
+
: (typeof ((_a = resolvedUserRecord.user_data) === null || _a === void 0 ? void 0 : _a.email) === 'string'
|
|
897
|
+
? (_b = resolvedUserRecord.user_data) === null || _b === void 0 ? void 0 : _b.email
|
|
898
|
+
: undefined)
|
|
899
|
+
}
|
|
900
|
+
: undefined;
|
|
901
|
+
addFunctionHistory({
|
|
902
|
+
ts: Date.now(),
|
|
903
|
+
name,
|
|
904
|
+
args: safeArgs,
|
|
905
|
+
runAsSystem: effectiveRunAsSystem,
|
|
906
|
+
user: userInfo
|
|
907
|
+
});
|
|
908
|
+
addEvent({
|
|
909
|
+
id: createEventId(),
|
|
910
|
+
ts: Date.now(),
|
|
911
|
+
type: 'function',
|
|
912
|
+
source: 'monit',
|
|
913
|
+
message: `invoke ${name}`,
|
|
914
|
+
data: sanitize({
|
|
915
|
+
args,
|
|
916
|
+
user: userInfo,
|
|
917
|
+
runAsSystem: effectiveRunAsSystem,
|
|
918
|
+
override: Boolean(overrideCode)
|
|
919
|
+
})
|
|
920
|
+
});
|
|
921
|
+
try {
|
|
922
|
+
const result = yield (0, context_1.GenerateContext)({
|
|
923
|
+
args,
|
|
924
|
+
app: appRef,
|
|
925
|
+
rules,
|
|
926
|
+
user: resolvedUser !== null && resolvedUser !== void 0 ? resolvedUser : { id: 'monitor', role: 'system' },
|
|
927
|
+
currentFunction: effectiveFunction,
|
|
928
|
+
functionsList,
|
|
929
|
+
services,
|
|
930
|
+
runAsSystem: effectiveRunAsSystem
|
|
931
|
+
});
|
|
932
|
+
return { result: sanitize(result) };
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
addEvent({
|
|
936
|
+
id: createEventId(),
|
|
937
|
+
ts: Date.now(),
|
|
938
|
+
type: 'error',
|
|
939
|
+
source: 'monit',
|
|
940
|
+
message: `invoke ${name} failed`,
|
|
941
|
+
data: sanitize({ error })
|
|
942
|
+
});
|
|
943
|
+
reply.code(500);
|
|
944
|
+
const details = getErrorDetails(error);
|
|
945
|
+
return { error: details.message, stack: details.stack };
|
|
946
|
+
}
|
|
947
|
+
}));
|
|
948
|
+
app.get(`${prefix}/api/users`, (req) => __awaiter(void 0, void 0, void 0, function* () {
|
|
949
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
950
|
+
const query = req.query;
|
|
951
|
+
const scope = (_a = query.scope) !== null && _a !== void 0 ? _a : 'all';
|
|
952
|
+
const rawSearch = typeof query.q === 'string' ? query.q.trim() : '';
|
|
953
|
+
const hasSearch = rawSearch.length > 0;
|
|
954
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
955
|
+
const searchRegex = hasSearch ? new RegExp(escapeRegex(rawSearch), 'i') : null;
|
|
956
|
+
const searchObjectId = hasSearch && mongodb_1.ObjectId.isValid(rawSearch) ? new mongodb_1.ObjectId(rawSearch) : null;
|
|
957
|
+
const parsedAuthLimit = Number((_c = (_b = query.authLimit) !== null && _b !== void 0 ? _b : query.limit) !== null && _c !== void 0 ? _c : 100);
|
|
958
|
+
const parsedCustomLimit = Number((_e = (_d = query.customLimit) !== null && _d !== void 0 ? _d : query.limit) !== null && _e !== void 0 ? _e : 25);
|
|
959
|
+
const parsedPage = Number((_g = (_f = query.customPage) !== null && _f !== void 0 ? _f : query.page) !== null && _g !== void 0 ? _g : 1);
|
|
960
|
+
const resolvedAuthLimit = Math.min(Number.isFinite(parsedAuthLimit) && parsedAuthLimit > 0 ? parsedAuthLimit : 100, 500);
|
|
961
|
+
const resolvedCustomLimit = Math.min(Number.isFinite(parsedCustomLimit) && parsedCustomLimit > 0 ? parsedCustomLimit : 25, 500);
|
|
962
|
+
const resolvedCustomPage = Math.max(Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1, 1);
|
|
963
|
+
const db = app.mongo.client.db(constants_1.DB_NAME);
|
|
964
|
+
const authCollection = (_h = constants_1.AUTH_CONFIG.authCollection) !== null && _h !== void 0 ? _h : 'auth_users';
|
|
965
|
+
const userCollection = constants_1.AUTH_CONFIG.userCollection;
|
|
966
|
+
const response = {
|
|
967
|
+
meta: {
|
|
968
|
+
userIdField: constants_1.AUTH_CONFIG.user_id_field,
|
|
969
|
+
authCollection,
|
|
970
|
+
customCollection: userCollection
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
if (scope === 'all' || scope === 'auth') {
|
|
974
|
+
const authFilter = hasSearch
|
|
975
|
+
? {
|
|
976
|
+
$or: [
|
|
977
|
+
...(searchObjectId ? [{ _id: searchObjectId }] : []),
|
|
978
|
+
{ email: searchRegex },
|
|
979
|
+
{ status: searchRegex }
|
|
980
|
+
]
|
|
981
|
+
}
|
|
982
|
+
: {};
|
|
983
|
+
const authItems = yield db
|
|
984
|
+
.collection(authCollection)
|
|
985
|
+
.find(authFilter)
|
|
986
|
+
.sort({ createdAt: -1, _id: -1 })
|
|
987
|
+
.limit(resolvedAuthLimit)
|
|
988
|
+
.toArray();
|
|
989
|
+
response.auth = {
|
|
990
|
+
collection: authCollection,
|
|
991
|
+
items: authItems.map((doc) => sanitize(doc))
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if ((scope === 'all' || scope === 'custom') && userCollection) {
|
|
995
|
+
const userIdField = (_j = constants_1.AUTH_CONFIG.user_id_field) !== null && _j !== void 0 ? _j : 'id';
|
|
996
|
+
const customFilter = hasSearch
|
|
997
|
+
? {
|
|
998
|
+
$or: [
|
|
999
|
+
...(searchObjectId ? [{ _id: searchObjectId }] : []),
|
|
1000
|
+
{ [userIdField]: searchRegex },
|
|
1001
|
+
{ email: searchRegex },
|
|
1002
|
+
{ name: searchRegex },
|
|
1003
|
+
{ username: searchRegex }
|
|
1004
|
+
]
|
|
1005
|
+
}
|
|
1006
|
+
: {};
|
|
1007
|
+
const total = yield db.collection(userCollection).countDocuments(customFilter);
|
|
1008
|
+
const totalPages = Math.max(1, Math.ceil(total / Math.max(resolvedCustomLimit, 1)));
|
|
1009
|
+
const page = Math.min(resolvedCustomPage, totalPages);
|
|
1010
|
+
const skip = Math.max(0, (page - 1) * resolvedCustomLimit);
|
|
1011
|
+
const customItems = yield db
|
|
1012
|
+
.collection(userCollection)
|
|
1013
|
+
.find(customFilter)
|
|
1014
|
+
.sort({ createdAt: -1, _id: -1 })
|
|
1015
|
+
.skip(skip)
|
|
1016
|
+
.limit(resolvedCustomLimit)
|
|
1017
|
+
.toArray();
|
|
1018
|
+
response.custom = {
|
|
1019
|
+
collection: userCollection,
|
|
1020
|
+
items: customItems.map((doc) => sanitize(doc)),
|
|
1021
|
+
pagination: {
|
|
1022
|
+
page,
|
|
1023
|
+
pages: totalPages,
|
|
1024
|
+
total,
|
|
1025
|
+
pageSize: resolvedCustomLimit
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
return response;
|
|
1030
|
+
}));
|
|
1031
|
+
app.post(`${prefix}/api/users`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1032
|
+
var _a, _b, _c;
|
|
1033
|
+
const body = req.body;
|
|
1034
|
+
const email = (_a = body === null || body === void 0 ? void 0 : body.email) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
1035
|
+
const password = body === null || body === void 0 ? void 0 : body.password;
|
|
1036
|
+
if (!email || !password) {
|
|
1037
|
+
reply.code(400);
|
|
1038
|
+
return { error: 'Missing email or password' };
|
|
1039
|
+
}
|
|
1040
|
+
const result = yield (0, handleUserRegistration_1.default)(app, {
|
|
1041
|
+
run_as_system: true,
|
|
1042
|
+
provider: handleUserRegistration_model_1.PROVIDER.LOCAL_USERPASS
|
|
1043
|
+
})({ email, password });
|
|
1044
|
+
const userId = (_b = result === null || result === void 0 ? void 0 : result.insertedId) === null || _b === void 0 ? void 0 : _b.toString();
|
|
1045
|
+
if (userId && constants_1.AUTH_CONFIG.userCollection && constants_1.AUTH_CONFIG.user_id_field) {
|
|
1046
|
+
const db = app.mongo.client.db(constants_1.DB_NAME);
|
|
1047
|
+
const customData = (_c = body === null || body === void 0 ? void 0 : body.customData) !== null && _c !== void 0 ? _c : {};
|
|
1048
|
+
yield db.collection(constants_1.AUTH_CONFIG.userCollection).updateOne({ [constants_1.AUTH_CONFIG.user_id_field]: userId }, {
|
|
1049
|
+
$set: Object.assign(Object.assign({}, customData), { [constants_1.AUTH_CONFIG.user_id_field]: userId })
|
|
1050
|
+
}, { upsert: true });
|
|
1051
|
+
}
|
|
1052
|
+
addEvent({
|
|
1053
|
+
id: createEventId(),
|
|
1054
|
+
ts: Date.now(),
|
|
1055
|
+
type: 'auth',
|
|
1056
|
+
source: 'monit',
|
|
1057
|
+
message: 'user created',
|
|
1058
|
+
data: sanitize({ email, userId })
|
|
1059
|
+
});
|
|
1060
|
+
reply.code(201);
|
|
1061
|
+
return { userId };
|
|
1062
|
+
}));
|
|
1063
|
+
app.patch(`${prefix}/api/users/:id/password`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1064
|
+
var _a;
|
|
1065
|
+
const params = req.params;
|
|
1066
|
+
const body = req.body;
|
|
1067
|
+
const password = body === null || body === void 0 ? void 0 : body.password;
|
|
1068
|
+
if (!password) {
|
|
1069
|
+
reply.code(400);
|
|
1070
|
+
return { error: 'Missing password' };
|
|
1071
|
+
}
|
|
1072
|
+
const db = app.mongo.client.db(constants_1.DB_NAME);
|
|
1073
|
+
const authCollection = (_a = constants_1.AUTH_CONFIG.authCollection) !== null && _a !== void 0 ? _a : 'auth_users';
|
|
1074
|
+
const selector = {};
|
|
1075
|
+
if (params.id && mongodb_1.ObjectId.isValid(params.id)) {
|
|
1076
|
+
selector._id = new mongodb_1.ObjectId(params.id);
|
|
1077
|
+
}
|
|
1078
|
+
else if (body === null || body === void 0 ? void 0 : body.email) {
|
|
1079
|
+
selector.email = body.email.toLowerCase();
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
reply.code(400);
|
|
1083
|
+
return { error: 'Invalid user identifier' };
|
|
1084
|
+
}
|
|
1085
|
+
const hashedPassword = yield (0, crypto_1.hashPassword)(password);
|
|
1086
|
+
const result = yield db.collection(authCollection).updateOne(selector, {
|
|
1087
|
+
$set: { password: hashedPassword }
|
|
1088
|
+
});
|
|
1089
|
+
if (!result.matchedCount) {
|
|
1090
|
+
reply.code(404);
|
|
1091
|
+
return { error: 'User not found' };
|
|
1092
|
+
}
|
|
1093
|
+
addEvent({
|
|
1094
|
+
id: createEventId(),
|
|
1095
|
+
ts: Date.now(),
|
|
1096
|
+
type: 'auth',
|
|
1097
|
+
source: 'monit',
|
|
1098
|
+
message: 'password updated',
|
|
1099
|
+
data: sanitize({ selector })
|
|
1100
|
+
});
|
|
1101
|
+
return { status: 'ok' };
|
|
1102
|
+
}));
|
|
1103
|
+
app.patch(`${prefix}/api/users/:id/status`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1104
|
+
var _a, _b;
|
|
1105
|
+
const params = req.params;
|
|
1106
|
+
const body = req.body;
|
|
1107
|
+
const db = app.mongo.client.db(constants_1.DB_NAME);
|
|
1108
|
+
const authCollection = (_a = constants_1.AUTH_CONFIG.authCollection) !== null && _a !== void 0 ? _a : 'auth_users';
|
|
1109
|
+
const selector = {};
|
|
1110
|
+
if (params.id && mongodb_1.ObjectId.isValid(params.id)) {
|
|
1111
|
+
selector._id = new mongodb_1.ObjectId(params.id);
|
|
1112
|
+
}
|
|
1113
|
+
else if (body === null || body === void 0 ? void 0 : body.email) {
|
|
1114
|
+
selector.email = body.email.toLowerCase();
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
reply.code(400);
|
|
1118
|
+
return { error: 'Invalid user identifier' };
|
|
1119
|
+
}
|
|
1120
|
+
const status = typeof (body === null || body === void 0 ? void 0 : body.disabled) === 'boolean'
|
|
1121
|
+
? (body.disabled ? 'disabled' : 'confirmed')
|
|
1122
|
+
: ((_b = body === null || body === void 0 ? void 0 : body.status) !== null && _b !== void 0 ? _b : 'disabled');
|
|
1123
|
+
const result = yield db.collection(authCollection).updateOne(selector, {
|
|
1124
|
+
$set: { status }
|
|
1125
|
+
});
|
|
1126
|
+
if (!result.matchedCount) {
|
|
1127
|
+
reply.code(404);
|
|
1128
|
+
return { error: 'User not found' };
|
|
1129
|
+
}
|
|
1130
|
+
addEvent({
|
|
1131
|
+
id: createEventId(),
|
|
1132
|
+
ts: Date.now(),
|
|
1133
|
+
type: 'auth',
|
|
1134
|
+
source: 'monit',
|
|
1135
|
+
message: `user status ${status}`,
|
|
1136
|
+
data: sanitize({ selector, status })
|
|
1137
|
+
});
|
|
1138
|
+
return { status: 'ok' };
|
|
1139
|
+
}));
|
|
1140
|
+
app.get(`${prefix}/api/collections`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1141
|
+
const db = app.mongo.client.db(constants_1.DB_NAME);
|
|
1142
|
+
const collections = yield db.listCollections().toArray();
|
|
1143
|
+
const items = collections
|
|
1144
|
+
.filter((entry) => !entry.name.startsWith('system.'))
|
|
1145
|
+
.map((entry) => ({
|
|
1146
|
+
name: entry.name,
|
|
1147
|
+
type: entry.type
|
|
1148
|
+
}));
|
|
1149
|
+
return { items };
|
|
1150
|
+
}));
|
|
1151
|
+
app.get(`${prefix}/api/collections/:name/rules`, (req) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1152
|
+
const params = req.params;
|
|
1153
|
+
const query = req.query;
|
|
1154
|
+
const rules = state_1.StateManager.select('rules');
|
|
1155
|
+
const runAsSystem = (query === null || query === void 0 ? void 0 : query.runAsSystem) === 'true';
|
|
1156
|
+
const resolvedUser = yield resolveUserContext(app, query === null || query === void 0 ? void 0 : query.userId);
|
|
1157
|
+
return buildCollectionRulesSnapshot(rules, params.name, resolvedUser, runAsSystem);
|
|
1158
|
+
}));
|
|
1159
|
+
app.get(`${prefix}/api/collections/history`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1160
|
+
return ({
|
|
1161
|
+
items: collectionHistory.slice(0, maxCollectionHistory)
|
|
1162
|
+
});
|
|
1163
|
+
}));
|
|
1164
|
+
app.post(`${prefix}/api/collections/query`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1165
|
+
var _a, _b;
|
|
1166
|
+
const body = req.body;
|
|
1167
|
+
const collection = body === null || body === void 0 ? void 0 : body.collection;
|
|
1168
|
+
if (!collection) {
|
|
1169
|
+
reply.code(400);
|
|
1170
|
+
return { error: 'Missing collection name' };
|
|
1171
|
+
}
|
|
1172
|
+
const rawQuery = (_a = body === null || body === void 0 ? void 0 : body.query) !== null && _a !== void 0 ? _a : {};
|
|
1173
|
+
if (Array.isArray(rawQuery) || typeof rawQuery !== 'object' || rawQuery === null) {
|
|
1174
|
+
reply.code(400);
|
|
1175
|
+
return { error: 'Query must be an object' };
|
|
1176
|
+
}
|
|
1177
|
+
const sort = body === null || body === void 0 ? void 0 : body.sort;
|
|
1178
|
+
if (sort !== undefined && !isPlainObject(sort)) {
|
|
1179
|
+
reply.code(400);
|
|
1180
|
+
return { error: 'Sort must be an object' };
|
|
1181
|
+
}
|
|
1182
|
+
const page = Math.max(1, Math.floor(Number((_b = body === null || body === void 0 ? void 0 : body.page) !== null && _b !== void 0 ? _b : 1) || 1));
|
|
1183
|
+
const skip = (page - 1) * COLLECTION_PAGE_SIZE;
|
|
1184
|
+
const rules = state_1.StateManager.select('rules');
|
|
1185
|
+
const services = state_1.StateManager.select('services');
|
|
1186
|
+
const resolvedUser = yield resolveUserContext(app, body === null || body === void 0 ? void 0 : body.userId);
|
|
1187
|
+
const runAsSystem = (body === null || body === void 0 ? void 0 : body.runAsSystem) !== false;
|
|
1188
|
+
const recordHistory = (body === null || body === void 0 ? void 0 : body.recordHistory) !== false;
|
|
1189
|
+
try {
|
|
1190
|
+
const mongoService = services['mongodb-atlas'](app, {
|
|
1191
|
+
rules,
|
|
1192
|
+
user: resolvedUser !== null && resolvedUser !== void 0 ? resolvedUser : {},
|
|
1193
|
+
run_as_system: runAsSystem
|
|
1194
|
+
});
|
|
1195
|
+
const options = {};
|
|
1196
|
+
if (isPlainObject(sort))
|
|
1197
|
+
options.sort = sort;
|
|
1198
|
+
const cursor = mongoService
|
|
1199
|
+
.db(constants_1.DB_NAME)
|
|
1200
|
+
.collection(collection)
|
|
1201
|
+
.find(rawQuery, undefined, Object.keys(options).length ? options : undefined)
|
|
1202
|
+
.skip(skip)
|
|
1203
|
+
.limit(COLLECTION_PAGE_SIZE + 1);
|
|
1204
|
+
const countPromise = mongoService
|
|
1205
|
+
.db(constants_1.DB_NAME)
|
|
1206
|
+
.collection(collection)
|
|
1207
|
+
.count(rawQuery);
|
|
1208
|
+
const [items, total] = yield Promise.all([cursor.toArray(), countPromise]);
|
|
1209
|
+
const hasMore = page * COLLECTION_PAGE_SIZE < total;
|
|
1210
|
+
const pageItems = items.length > COLLECTION_PAGE_SIZE
|
|
1211
|
+
? items.slice(0, COLLECTION_PAGE_SIZE)
|
|
1212
|
+
: items;
|
|
1213
|
+
if (recordHistory) {
|
|
1214
|
+
addCollectionHistory({
|
|
1215
|
+
ts: Date.now(),
|
|
1216
|
+
collection,
|
|
1217
|
+
mode: 'query',
|
|
1218
|
+
query: sanitize(rawQuery),
|
|
1219
|
+
sort: sort ? sanitize(sort) : undefined,
|
|
1220
|
+
runAsSystem,
|
|
1221
|
+
user: getUserInfo(resolvedUser),
|
|
1222
|
+
page
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
items: sanitize(pageItems),
|
|
1227
|
+
count: pageItems.length,
|
|
1228
|
+
total,
|
|
1229
|
+
page,
|
|
1230
|
+
pageSize: COLLECTION_PAGE_SIZE,
|
|
1231
|
+
hasMore
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
const details = getErrorDetails(error);
|
|
1236
|
+
reply.code(500);
|
|
1237
|
+
return { error: details.message, stack: details.stack };
|
|
1238
|
+
}
|
|
1239
|
+
}));
|
|
1240
|
+
app.post(`${prefix}/api/collections/aggregate`, (req, reply) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1241
|
+
var _a, _b, _c, _d;
|
|
1242
|
+
const body = req.body;
|
|
1243
|
+
const collection = body === null || body === void 0 ? void 0 : body.collection;
|
|
1244
|
+
if (!collection) {
|
|
1245
|
+
reply.code(400);
|
|
1246
|
+
return { error: 'Missing collection name' };
|
|
1247
|
+
}
|
|
1248
|
+
const rawPipeline = (_a = body === null || body === void 0 ? void 0 : body.pipeline) !== null && _a !== void 0 ? _a : [];
|
|
1249
|
+
if (!Array.isArray(rawPipeline)) {
|
|
1250
|
+
reply.code(400);
|
|
1251
|
+
return { error: 'Aggregate pipeline must be an array' };
|
|
1252
|
+
}
|
|
1253
|
+
const sort = body === null || body === void 0 ? void 0 : body.sort;
|
|
1254
|
+
if (sort !== undefined && !isPlainObject(sort)) {
|
|
1255
|
+
reply.code(400);
|
|
1256
|
+
return { error: 'Sort must be an object' };
|
|
1257
|
+
}
|
|
1258
|
+
const page = Math.max(1, Math.floor(Number((_b = body === null || body === void 0 ? void 0 : body.page) !== null && _b !== void 0 ? _b : 1) || 1));
|
|
1259
|
+
const skip = (page - 1) * COLLECTION_PAGE_SIZE;
|
|
1260
|
+
const rules = state_1.StateManager.select('rules');
|
|
1261
|
+
const services = state_1.StateManager.select('services');
|
|
1262
|
+
const resolvedUser = yield resolveUserContext(app, body === null || body === void 0 ? void 0 : body.userId);
|
|
1263
|
+
const runAsSystem = (body === null || body === void 0 ? void 0 : body.runAsSystem) !== false;
|
|
1264
|
+
const recordHistory = (body === null || body === void 0 ? void 0 : body.recordHistory) !== false;
|
|
1265
|
+
try {
|
|
1266
|
+
const pipeline = [...rawPipeline];
|
|
1267
|
+
if (sort)
|
|
1268
|
+
pipeline.push({ $sort: sort });
|
|
1269
|
+
if (skip > 0)
|
|
1270
|
+
pipeline.push({ $skip: skip });
|
|
1271
|
+
pipeline.push({ $limit: COLLECTION_PAGE_SIZE + 1 });
|
|
1272
|
+
const mongoService = services['mongodb-atlas'](app, {
|
|
1273
|
+
rules,
|
|
1274
|
+
user: resolvedUser !== null && resolvedUser !== void 0 ? resolvedUser : {},
|
|
1275
|
+
run_as_system: runAsSystem
|
|
1276
|
+
});
|
|
1277
|
+
const cursor = mongoService
|
|
1278
|
+
.db(constants_1.DB_NAME)
|
|
1279
|
+
.collection(collection)
|
|
1280
|
+
.aggregate(pipeline, undefined, true);
|
|
1281
|
+
const countCursor = mongoService
|
|
1282
|
+
.db(constants_1.DB_NAME)
|
|
1283
|
+
.collection(collection)
|
|
1284
|
+
.aggregate([...rawPipeline, { $count: 'total' }], undefined, true);
|
|
1285
|
+
const [items, totalResult] = yield Promise.all([cursor.toArray(), countCursor.toArray()]);
|
|
1286
|
+
const total = (_d = (_c = totalResult === null || totalResult === void 0 ? void 0 : totalResult[0]) === null || _c === void 0 ? void 0 : _c.total) !== null && _d !== void 0 ? _d : 0;
|
|
1287
|
+
const hasMore = page * COLLECTION_PAGE_SIZE < total;
|
|
1288
|
+
const pageItems = items.length > COLLECTION_PAGE_SIZE
|
|
1289
|
+
? items.slice(0, COLLECTION_PAGE_SIZE)
|
|
1290
|
+
: items;
|
|
1291
|
+
if (recordHistory) {
|
|
1292
|
+
addCollectionHistory({
|
|
1293
|
+
ts: Date.now(),
|
|
1294
|
+
collection,
|
|
1295
|
+
mode: 'aggregate',
|
|
1296
|
+
pipeline: sanitize(rawPipeline),
|
|
1297
|
+
sort: sort ? sanitize(sort) : undefined,
|
|
1298
|
+
runAsSystem,
|
|
1299
|
+
user: getUserInfo(resolvedUser),
|
|
1300
|
+
page
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
items: sanitize(pageItems),
|
|
1305
|
+
count: pageItems.length,
|
|
1306
|
+
total,
|
|
1307
|
+
page,
|
|
1308
|
+
pageSize: COLLECTION_PAGE_SIZE,
|
|
1309
|
+
hasMore
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
const details = getErrorDetails(error);
|
|
1314
|
+
reply.code(500);
|
|
1315
|
+
return { error: details.message, stack: details.stack };
|
|
1316
|
+
}
|
|
1317
|
+
}));
|
|
1318
|
+
}), { name: 'monitoring' });
|
|
1319
|
+
exports.default = createMonitoringPlugin;
|