apostrophe 3.53.0 → 3.55.0
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/CHANGELOG.md +58 -1
- package/defaults.js +1 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextModeAndSettings.vue +5 -2
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +28 -19
- package/modules/@apostrophecms/any-doc-type/index.js +2 -2
- package/modules/@apostrophecms/any-page-type/index.js +2 -2
- package/modules/@apostrophecms/doc/index.js +55 -29
- package/modules/@apostrophecms/doc-type/index.js +11 -6
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +4 -440
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +445 -0
- package/modules/@apostrophecms/i18n/i18n/de.json +113 -105
- package/modules/@apostrophecms/i18n/i18n/es.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/fr.json +8 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +10 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +8 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +1 -0
- package/modules/@apostrophecms/log/index.js +429 -0
- package/modules/@apostrophecms/login/index.js +47 -4
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +14 -1
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +1 -1
- package/modules/@apostrophecms/module/index.js +32 -6
- package/modules/@apostrophecms/module/lib/log.js +68 -0
- package/modules/@apostrophecms/page/index.js +71 -19
- package/modules/@apostrophecms/page/lib/legacy-migrations.js +0 -57
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +8 -285
- package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +291 -0
- package/modules/@apostrophecms/page-type/index.js +39 -26
- package/modules/@apostrophecms/piece-type/index.js +19 -11
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +2 -357
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +2 -86
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +2 -254
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +2 -77
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputBoolean.vue +2 -44
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +2 -64
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +2 -94
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +3 -47
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -82
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +2 -37
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +2 -26
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -57
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +2 -259
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +2 -38
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +2 -275
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +2 -167
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +2 -115
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +3 -279
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +2 -83
- package/modules/@apostrophecms/schema/ui/apos/lib/detectChange.js +10 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +361 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +89 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +257 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputAttachment.js +81 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputBoolean.js +48 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +68 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputColor.js +98 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputDateAndTime.js +49 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +86 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputPassword.js +41 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +29 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRange.js +60 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +262 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js +41 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +278 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +170 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +118 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +281 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSearchList.js +85 -0
- package/modules/@apostrophecms/template/index.js +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -2
- package/modules/@apostrophecms/util/index.js +83 -13
- package/modules/@apostrophecms/util/lib/logger.js +19 -17
- package/package.json +1 -1
- package/test/docs.js +35 -2
- package/test/log.js +1765 -0
- package/test/pages.js +57 -0
- package/test-lib/util.js +1 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// Structured logging for Apostrophe.
|
|
2
|
+
//
|
|
3
|
+
// This module is generic, low level implementation. For logging inside of a module,
|
|
4
|
+
// see `logInfo`, `logError`, etc. methods available in every module
|
|
5
|
+
// via the base class, @apostrophecms/module.
|
|
6
|
+
//
|
|
7
|
+
// ### `logger`
|
|
8
|
+
//
|
|
9
|
+
// Optional. It can be an object or a function.
|
|
10
|
+
// If a function it accepts `apos` and returns an object with
|
|
11
|
+
// at least `info`, `debug`, `warn` and `error` methods. If a `destroy` method
|
|
12
|
+
// is present it will be invoked and awaited (Promise) when Apostrophe is shut down.
|
|
13
|
+
// The object, or the returned object, must have `info`, `debug`, `warn` and `error` methods.
|
|
14
|
+
// If `destroy` is present it will be invoked and awaited (Promise) when Apostrophe is shut down.
|
|
15
|
+
// If this option is not supplied, logs are simply written to the Node.js `console`.
|
|
16
|
+
// Calls to `apos.utils.info`, `apos.utils.error`, etc. or module level `self.logInfo`,
|
|
17
|
+
// `self.logError`, etc are routed through this object by Apostrophe.
|
|
18
|
+
// This provides compatibility out of the box with many popular
|
|
19
|
+
// logging modules, including`pino`, `winston`, etc.
|
|
20
|
+
//
|
|
21
|
+
// ## Options
|
|
22
|
+
//
|
|
23
|
+
// ### `messageAs`
|
|
24
|
+
//
|
|
25
|
+
// When the messageAs option is set, the message argument to apos.util.info, etc.
|
|
26
|
+
// is bundled into the second, object - based argument as a property of the name
|
|
27
|
+
// given, and only the object argument is passed to the `logger`, which is useful
|
|
28
|
+
// if using Pino.
|
|
29
|
+
// If there is no object-based argument an object is created.
|
|
30
|
+
// Example:
|
|
31
|
+
// ```js
|
|
32
|
+
// {
|
|
33
|
+
// options: {
|
|
34
|
+
// messageAs: 'msg'
|
|
35
|
+
// }
|
|
36
|
+
// }
|
|
37
|
+
// ```
|
|
38
|
+
// The resulting util log call will be:
|
|
39
|
+
// ```js
|
|
40
|
+
// self.apos.util.error({
|
|
41
|
+
// msg: '@apostrophecms/login: incorrect-username: User admin failed to log in',
|
|
42
|
+
// type: 'incorrect-username'
|
|
43
|
+
// module: '@apostrophecms/login'
|
|
44
|
+
// });
|
|
45
|
+
// ```
|
|
46
|
+
//
|
|
47
|
+
// ### `filter`
|
|
48
|
+
//
|
|
49
|
+
// By module name, or `*` we can specify any mix of severity levels and
|
|
50
|
+
// specific event types, and entries are kept if *either* criterion is met.
|
|
51
|
+
// Example:
|
|
52
|
+
// ```js
|
|
53
|
+
// {
|
|
54
|
+
// options: {
|
|
55
|
+
// filter: {
|
|
56
|
+
// // Log all errors and warnings from any module
|
|
57
|
+
// '*': {
|
|
58
|
+
// severity: [ 'warn', 'error' ]
|
|
59
|
+
// // match all severity levels
|
|
60
|
+
// // severity: '*'
|
|
61
|
+
// // match event types
|
|
62
|
+
// // events: [ 'event-type-1', 'event-type-2' ]
|
|
63
|
+
// // match all event types
|
|
64
|
+
// // events: '*'
|
|
65
|
+
// },
|
|
66
|
+
// // Log specific event types from the login module
|
|
67
|
+
// '@apostrophecms/login': {
|
|
68
|
+
// events: [ 'incorrect-username', 'incorrect-password' ]
|
|
69
|
+
// // match all event types
|
|
70
|
+
// // events: '*'
|
|
71
|
+
// // match specific severity levels
|
|
72
|
+
// // severity: [ 'info' ]
|
|
73
|
+
// // match all severity levels
|
|
74
|
+
// // severity: '*'
|
|
75
|
+
// }
|
|
76
|
+
// }
|
|
77
|
+
// }
|
|
78
|
+
// }
|
|
79
|
+
// ```
|
|
80
|
+
// In this example, all errors and warnings from any module, but
|
|
81
|
+
// only the specific event types (no matter the severity) from the login
|
|
82
|
+
// module, are logged. The logs will be kept if *either* criterion is met.
|
|
83
|
+
// `filter['*'] = true` enables logging of all events from all modules.
|
|
84
|
+
//
|
|
85
|
+
// ## Environment Variables
|
|
86
|
+
//
|
|
87
|
+
// ### `APOS_FILTER_LOGS`
|
|
88
|
+
//
|
|
89
|
+
// If set, this environment variable overrides the `filter` option.
|
|
90
|
+
// Example:
|
|
91
|
+
// ```sh
|
|
92
|
+
// # same as the `filter` example above
|
|
93
|
+
// export APOS_FILTER_LOGS='*:severity:warn,error;@apostrophecms/login:events:incorrect-username,incorrect-password'
|
|
94
|
+
// # log everything, analogous to `{ filter: { '*': true }}`
|
|
95
|
+
// export APOS_FILTER_LOGS='*'
|
|
96
|
+
// ```
|
|
97
|
+
//
|
|
98
|
+
|
|
99
|
+
const _ = require('lodash');
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
options: {
|
|
103
|
+
alias: 'structuredLog'
|
|
104
|
+
},
|
|
105
|
+
init(self) {
|
|
106
|
+
self.filters = {};
|
|
107
|
+
self.filterCache = {};
|
|
108
|
+
self.initFilters();
|
|
109
|
+
},
|
|
110
|
+
methods(self) {
|
|
111
|
+
// Keep those inside the methods because of performance.
|
|
112
|
+
// Do not move to the root because of tests.
|
|
113
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
114
|
+
const isTest = self.options.apos?.options?.test;
|
|
115
|
+
const utilInspect = require('node:util').inspect;
|
|
116
|
+
|
|
117
|
+
const formatDevObj = isTest
|
|
118
|
+
? (obj) => JSON.stringify(obj, null, 2)
|
|
119
|
+
: (obj) => utilInspect(obj, {
|
|
120
|
+
depth: null,
|
|
121
|
+
colors: true
|
|
122
|
+
});
|
|
123
|
+
const formatObj = isProduction
|
|
124
|
+
? JSON.stringify
|
|
125
|
+
: formatDevObj;
|
|
126
|
+
const formatString = !isProduction
|
|
127
|
+
? (str, args) => (args.length > 1) ? str.trim() + '\n' : str
|
|
128
|
+
: (str) => str;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
// Stringify object arguments. If `NODE_ENV` is not `production`,
|
|
132
|
+
// pretty print the objects and add a new line at the end of string arguments.
|
|
133
|
+
// This method is meant to be used from the methods of a custom `logger`.
|
|
134
|
+
// See the default logger implementation in `util/lib/logger.js` for an example.
|
|
135
|
+
formatLogByEnv(args) {
|
|
136
|
+
return args.map((arg) => {
|
|
137
|
+
if (typeof arg === 'string') {
|
|
138
|
+
return formatString(arg, args);
|
|
139
|
+
}
|
|
140
|
+
if (_.isPlainObject(arg)) {
|
|
141
|
+
return formatObj(arg);
|
|
142
|
+
}
|
|
143
|
+
return arg;
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
// Normalize the filters. Detect configuration and set defaults
|
|
147
|
+
// per the current NODE_ENV if needed.
|
|
148
|
+
// Convert `{ *: true }` to an object with all severity levels.
|
|
149
|
+
// Convert severity wildcards to arrays of (all) severity levels. This
|
|
150
|
+
// speeds up the severity detection (no wildcards match).
|
|
151
|
+
// Convert eventType wildcards to `[ '*' ]` array.
|
|
152
|
+
// Override the configuration with the `APOS_FILTER_LOGS` environment variable
|
|
153
|
+
// if set.
|
|
154
|
+
initFilters() {
|
|
155
|
+
self.filters = self.options.filter || {};
|
|
156
|
+
if (process.env.APOS_FILTER_LOGS) {
|
|
157
|
+
try {
|
|
158
|
+
self.filters = self.parseEnvFilter(process.env.APOS_FILTER_LOGS);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
throw new Error(`Invalid APOS_FILTER_LOGS environment variable: ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
self.filters['*'] = self.filters['*'] || {};
|
|
164
|
+
|
|
165
|
+
// Transform *: true - log absolutely everything.
|
|
166
|
+
if (self.filters['*'] === true) {
|
|
167
|
+
self.filters['*'] = {
|
|
168
|
+
severity: self.getDefaultSeverity(false)
|
|
169
|
+
};
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Add environment specific severity levels if no severity is specified.
|
|
173
|
+
if (!self.filters['*'].severity) {
|
|
174
|
+
self.filters['*'] = {
|
|
175
|
+
...self.filters['*'],
|
|
176
|
+
severity: self.getDefaultSeverity(isProduction)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle wildcards and validate.
|
|
181
|
+
for (const [ module, config ] of Object.entries(self.filters)) {
|
|
182
|
+
for (const [ type, value ] of Object.entries(config)) {
|
|
183
|
+
if (value === true || value === '*') {
|
|
184
|
+
config[type] = (type === 'severity')
|
|
185
|
+
? self.getDefaultSeverity(false)
|
|
186
|
+
: [ '*' ];
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(config[type])) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Invalid ${type} filter for module ${module}: ${JSON.stringify(config[type])}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
// Convert a string filter configuration to an object.
|
|
197
|
+
// Example:
|
|
198
|
+
// `*:severity:warn,error;@apostrophecms/login:events:success,failure`
|
|
199
|
+
// results in:
|
|
200
|
+
// {
|
|
201
|
+
// '*': {
|
|
202
|
+
// severity: [ 'warn', 'error' ]
|
|
203
|
+
// },
|
|
204
|
+
// '@apostrophecms/login': {
|
|
205
|
+
// events: [ 'success', 'failure' ]
|
|
206
|
+
// }
|
|
207
|
+
// }
|
|
208
|
+
// Log all is just `*` and results in `{ '*': true }`.
|
|
209
|
+
parseEnvFilter(envFilter) {
|
|
210
|
+
const filter = {};
|
|
211
|
+
if (envFilter === '*') {
|
|
212
|
+
filter['*'] = true;
|
|
213
|
+
return filter;
|
|
214
|
+
}
|
|
215
|
+
envFilter.split(';').forEach((entry) => {
|
|
216
|
+
const [ module, ...criteria ] = entry
|
|
217
|
+
.trim()
|
|
218
|
+
.split(':')
|
|
219
|
+
.map((str) => str.trim());
|
|
220
|
+
// Trnasform e.g. `events:ev1,ev2` to`{ events: [ 'ev1', 'ev2' ] }`.
|
|
221
|
+
filter[module] = criteria.reduce((acc, current, index, arr) => {
|
|
222
|
+
if (index % 2) {
|
|
223
|
+
return acc;
|
|
224
|
+
}
|
|
225
|
+
if (!arr[index + 1] || typeof arr[index + 1] !== 'string') {
|
|
226
|
+
throw new Error(`Malformed configuration for module "${module}".`);
|
|
227
|
+
}
|
|
228
|
+
acc[current] = arr[index + 1].split(',').map((str) => str.trim());
|
|
229
|
+
return acc;
|
|
230
|
+
}, {});
|
|
231
|
+
});
|
|
232
|
+
return filter;
|
|
233
|
+
},
|
|
234
|
+
getDefaultSeverity(forProd = false) {
|
|
235
|
+
return forProd
|
|
236
|
+
? [ 'warn', 'error' ]
|
|
237
|
+
: [ 'debug', 'info', 'warn', 'error' ];
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// Internal method, do not use it directly. See `@apostrophecms/module` for
|
|
241
|
+
// module level logging methods - logInfo, logError, etc.
|
|
242
|
+
// `moduleSelf` is the module `self` object.
|
|
243
|
+
// The allowed `severity` levels are `debug`, `info`, `warn` and `error`.
|
|
244
|
+
// `severity` is required.
|
|
245
|
+
// `req` (optional) is an apos request object.
|
|
246
|
+
// The implementor will suport the following signature:
|
|
247
|
+
// - (eventType)
|
|
248
|
+
// - (eventType, data)
|
|
249
|
+
// - (eventType, message)
|
|
250
|
+
// - (eventType, message, data)
|
|
251
|
+
// - (req, eventType[, message, data]) where message and data are optional
|
|
252
|
+
logEntry(moduleSelf, severity, req, eventType, message, data) {
|
|
253
|
+
if (!self.shouldKeepEntry(moduleSelf, severity, req, eventType)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const args = self.processLoggerArgs(
|
|
257
|
+
moduleSelf,
|
|
258
|
+
severity,
|
|
259
|
+
req,
|
|
260
|
+
eventType,
|
|
261
|
+
message,
|
|
262
|
+
data
|
|
263
|
+
);
|
|
264
|
+
self.apos.util[severity](...args);
|
|
265
|
+
},
|
|
266
|
+
// An internal method, do not use it directly.
|
|
267
|
+
// Do a minimal computation and validation of the arguments - logs should
|
|
268
|
+
// be fast. The function exepects 4 or 5 arguments (without counting the first optional `moduleSelf` argument),
|
|
269
|
+
// but all of the below will work:
|
|
270
|
+
// processLogArgs(moduleSelf, severity = 'info', eventType = 'event-type');
|
|
271
|
+
// processLogArgs(moduleSelf, severity = 'info', eventType = 'event-type', data = { module: 'my-module' });
|
|
272
|
+
// processLogArgs(moduleSelf, severity = 'info', eventType = 'event-type', message = 'message', data = { module: 'my-module' });
|
|
273
|
+
// processLogArgs(moduleSelf, severity = 'info', req, eventType = 'event-type', [...messageAndOrData]);
|
|
274
|
+
//
|
|
275
|
+
// If `moduleSelf` is provided, it is used to extract the module name from
|
|
276
|
+
// the `__meta` property.
|
|
277
|
+
// `severity` and `eventType` are required.
|
|
278
|
+
// `req`, `message` and `data` are optional.
|
|
279
|
+
// `req` is an apos request object. If provided, the `data` argument will be
|
|
280
|
+
// enriched with additional information from the request.
|
|
281
|
+
// `data.module` is recognized as a special property and is used to refortmat
|
|
282
|
+
// the message.
|
|
283
|
+
// `data.severity` is always set to the value of the `severity` argument.
|
|
284
|
+
// `data.type` is always set to the value of the `eventType` argument.
|
|
285
|
+
// message is optional, if not provided it is generated from the eventType and data.module.
|
|
286
|
+
// Optional message values:
|
|
287
|
+
// - `module: eventType: message`
|
|
288
|
+
// - `eventType: message`: (missing module)
|
|
289
|
+
// - `module: eventType`: (missing message)
|
|
290
|
+
// - `eventType`: (missing module and message)
|
|
291
|
+
//
|
|
292
|
+
// Returns [ data ] or [ message, data ] depending on option.messageAs.
|
|
293
|
+
// `data` is always an object containing at least a `type` and `severity` properties.
|
|
294
|
+
processLoggerArgs(moduleSelf, severity, ...args) {
|
|
295
|
+
let req;
|
|
296
|
+
let eventType;
|
|
297
|
+
let message;
|
|
298
|
+
let obj;
|
|
299
|
+
const data = {};
|
|
300
|
+
|
|
301
|
+
// Detect `req` argument with a simple duck type check - apos `req` object
|
|
302
|
+
// has always a translate `t` function.
|
|
303
|
+
if (args[0] && typeof args[0].t === 'function') {
|
|
304
|
+
[ req, eventType, message, obj ] = args;
|
|
305
|
+
} else {
|
|
306
|
+
[ eventType, message, obj ] = args;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (typeof severity !== 'string') {
|
|
310
|
+
throw new Error('Severity must be a string.');
|
|
311
|
+
}
|
|
312
|
+
if (typeof eventType !== 'string') {
|
|
313
|
+
throw new Error('Event type must be a string');
|
|
314
|
+
}
|
|
315
|
+
if (_.isPlainObject(message)) {
|
|
316
|
+
obj = message;
|
|
317
|
+
message = undefined;
|
|
318
|
+
}
|
|
319
|
+
obj = obj ? { ...obj } : {};
|
|
320
|
+
const aposModule = moduleSelf.__meta?.name ?? '__unknown__';
|
|
321
|
+
|
|
322
|
+
if (typeof message === 'string' && message.trim().length > 0) {
|
|
323
|
+
message = aposModule
|
|
324
|
+
? `${aposModule}: ${eventType}: ${message}`
|
|
325
|
+
: `${eventType}: ${message}`;
|
|
326
|
+
} else {
|
|
327
|
+
message = aposModule
|
|
328
|
+
? `${aposModule}: ${eventType}`
|
|
329
|
+
: eventType;
|
|
330
|
+
}
|
|
331
|
+
if (self.options.messageAs) {
|
|
332
|
+
data[self.options.messageAs] = message;
|
|
333
|
+
delete obj[self.options.messageAs];
|
|
334
|
+
}
|
|
335
|
+
// Preserve the property order.
|
|
336
|
+
data.module = aposModule;
|
|
337
|
+
data.type = eventType;
|
|
338
|
+
data.severity = severity;
|
|
339
|
+
|
|
340
|
+
self.processRequestData(req, data);
|
|
341
|
+
// Don't override system properties.
|
|
342
|
+
Object.assign(data, obj);
|
|
343
|
+
data.module = aposModule;
|
|
344
|
+
data.type = eventType;
|
|
345
|
+
data.severity = severity;
|
|
346
|
+
|
|
347
|
+
return self.options.messageAs ? [ data ] : [ message, data ];
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
// Enrich the `data` argument with additional information from the request.
|
|
351
|
+
processRequestData(req, data) {
|
|
352
|
+
if (!req) {
|
|
353
|
+
return data;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
data.url = req.originalUrl;
|
|
357
|
+
data.path = req.path;
|
|
358
|
+
data.method = req.method;
|
|
359
|
+
data.ip = self.getIp(req);
|
|
360
|
+
data.query = req.query;
|
|
361
|
+
data.requestId = self.getRequestId(req);
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// Helper to get the IP address from the request.
|
|
365
|
+
// Can be overriden.
|
|
366
|
+
getIp(req) {
|
|
367
|
+
return req.ip;
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// Helper to get unique request id. It will be generated (once) if not present.
|
|
371
|
+
// Can be refatored to express level in the future.
|
|
372
|
+
getRequestId(req) {
|
|
373
|
+
if (!req.requestId) {
|
|
374
|
+
req.requestId = self.apos.util.generateId();
|
|
375
|
+
}
|
|
376
|
+
return req.requestId;
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// Assess the module filter configuration and determine if the log
|
|
380
|
+
// should be kept or rejected.
|
|
381
|
+
//
|
|
382
|
+
// `moduleSelf` and `args` arguments should be the same as
|
|
383
|
+
// passed to `processLogArgs(...)`.
|
|
384
|
+
//
|
|
385
|
+
// The module and global configs are merged.
|
|
386
|
+
// The logic is as follows:
|
|
387
|
+
// - if severity is matched, keep the log (no matter the event type)
|
|
388
|
+
// - if eventType is matched, keep the log (no matter the severity)
|
|
389
|
+
shouldKeepEntry(moduleSelf, ...args) {
|
|
390
|
+
// Detect severity and eventType from the arguments.
|
|
391
|
+
let severity;
|
|
392
|
+
let req;
|
|
393
|
+
let eventType;
|
|
394
|
+
if (args[1] && typeof args[1].t === 'function') {
|
|
395
|
+
// eslint-disable-next-line no-unused-vars
|
|
396
|
+
[ severity, req, eventType ] = args;
|
|
397
|
+
} else {
|
|
398
|
+
[ severity, eventType ] = args;
|
|
399
|
+
}
|
|
400
|
+
const aposModule = moduleSelf.__meta?.name ?? '__unknown__';
|
|
401
|
+
const cacheId = `${aposModule}:${severity}:${eventType}`;
|
|
402
|
+
if (typeof self.filterCache[cacheId] !== 'undefined') {
|
|
403
|
+
return self.filterCache[cacheId];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Consolidate and match severity and event type.
|
|
407
|
+
const severityArr = [
|
|
408
|
+
...new Set([
|
|
409
|
+
...self.filters['*'].severity,
|
|
410
|
+
...(self.filters[aposModule]?.severity || [])
|
|
411
|
+
])
|
|
412
|
+
];
|
|
413
|
+
const eventsArr = [
|
|
414
|
+
...new Set([
|
|
415
|
+
...(self.filters['*'].events || []),
|
|
416
|
+
...(self.filters[aposModule]?.events || [])
|
|
417
|
+
])
|
|
418
|
+
];
|
|
419
|
+
const severityMatch = severityArr.includes(severity);
|
|
420
|
+
const eventsMatch = eventsArr.length > 0
|
|
421
|
+
? eventsArr.includes(eventType) || eventsArr.includes('*')
|
|
422
|
+
: severityMatch;
|
|
423
|
+
|
|
424
|
+
self.filterCache[cacheId] = severityMatch || eventsMatch;
|
|
425
|
+
return self.filterCache[cacheId];
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
};
|
|
@@ -480,8 +480,10 @@ module.exports = {
|
|
|
480
480
|
//
|
|
481
481
|
// If the user's login SUCCEEDS, the return value is
|
|
482
482
|
// the `user` object.
|
|
483
|
+
// `attempts`, `ip` and `requestId` are optional, sent for only logging needs. They won't
|
|
484
|
+
// be available with passport.
|
|
483
485
|
|
|
484
|
-
async verifyLogin(username, password) {
|
|
486
|
+
async verifyLogin(username, password, attempts = 0, ip, requestId) {
|
|
485
487
|
const req = self.apos.task.getReq();
|
|
486
488
|
const user = await self.apos.user.find(req, {
|
|
487
489
|
$or: [
|
|
@@ -492,14 +494,32 @@ module.exports = {
|
|
|
492
494
|
}).toObject();
|
|
493
495
|
|
|
494
496
|
if (!user) {
|
|
497
|
+
self.logInfo('incorrect-username', {
|
|
498
|
+
username,
|
|
499
|
+
ip,
|
|
500
|
+
attempts: attempts + 1,
|
|
501
|
+
requestId
|
|
502
|
+
});
|
|
495
503
|
await Promise.delay(1000);
|
|
496
504
|
return false;
|
|
497
505
|
}
|
|
498
506
|
try {
|
|
499
507
|
await self.apos.user.verifyPassword(user, password);
|
|
508
|
+
self.logInfo('correct-password', {
|
|
509
|
+
username,
|
|
510
|
+
ip,
|
|
511
|
+
attempts: attempts,
|
|
512
|
+
requestId
|
|
513
|
+
});
|
|
500
514
|
return user;
|
|
501
515
|
} catch (err) {
|
|
502
516
|
if (err.name === 'invalid') {
|
|
517
|
+
self.logInfo('incorrect-password', {
|
|
518
|
+
username,
|
|
519
|
+
ip,
|
|
520
|
+
attempts: attempts + 1,
|
|
521
|
+
requestId
|
|
522
|
+
});
|
|
503
523
|
await Promise.delay(1000);
|
|
504
524
|
return false;
|
|
505
525
|
} else {
|
|
@@ -642,6 +662,10 @@ module.exports = {
|
|
|
642
662
|
_id: token.userId
|
|
643
663
|
});
|
|
644
664
|
await self.passportLogin(req, user);
|
|
665
|
+
// No access to login attempts in the final phase.
|
|
666
|
+
self.logInfo(req, 'complete', {
|
|
667
|
+
username: user.username
|
|
668
|
+
});
|
|
645
669
|
} else {
|
|
646
670
|
delete token.requirementsToVerify;
|
|
647
671
|
self.bearerTokens.updateOne(token, {
|
|
@@ -649,6 +673,9 @@ module.exports = {
|
|
|
649
673
|
requirementsToVerify: 1
|
|
650
674
|
}
|
|
651
675
|
});
|
|
676
|
+
self.logInfo(req, 'complete', {
|
|
677
|
+
username: user.username
|
|
678
|
+
});
|
|
652
679
|
return {
|
|
653
680
|
token
|
|
654
681
|
};
|
|
@@ -697,6 +724,7 @@ module.exports = {
|
|
|
697
724
|
}
|
|
698
725
|
|
|
699
726
|
const { cachedAttempts, reached } = await self.checkLoginAttempts(username);
|
|
727
|
+
const logAttempts = cachedAttempts ?? 0;
|
|
700
728
|
|
|
701
729
|
if (reached) {
|
|
702
730
|
throw self.apos.error('invalid', req.t('apostrophe:loginMaxAttemptsReached', {
|
|
@@ -716,7 +744,14 @@ module.exports = {
|
|
|
716
744
|
throw e;
|
|
717
745
|
}
|
|
718
746
|
}
|
|
719
|
-
|
|
747
|
+
// send log information
|
|
748
|
+
const user = await self.apos.login.verifyLogin(
|
|
749
|
+
username,
|
|
750
|
+
password,
|
|
751
|
+
logAttempts,
|
|
752
|
+
self.apos.structuredLog.getIp(req),
|
|
753
|
+
self.apos.structuredLog.getRequestId(req)
|
|
754
|
+
);
|
|
720
755
|
if (!user) {
|
|
721
756
|
// For security reasons we may not tell the user which case applies
|
|
722
757
|
throw self.apos.error('invalid', req.t('apostrophe:loginPageBadCredentials'));
|
|
@@ -727,7 +762,7 @@ module.exports = {
|
|
|
727
762
|
if (requirementsToVerify.length) {
|
|
728
763
|
const token = cuid();
|
|
729
764
|
|
|
730
|
-
await self.bearerTokens.
|
|
765
|
+
await self.bearerTokens.insertOne({
|
|
731
766
|
_id: token,
|
|
732
767
|
userId: user._id,
|
|
733
768
|
requirementsToVerify,
|
|
@@ -747,16 +782,24 @@ module.exports = {
|
|
|
747
782
|
if (session) {
|
|
748
783
|
await self.passportLogin(req, user);
|
|
749
784
|
await self.clearLoginAttempts(user.username);
|
|
785
|
+
self.logInfo(req, 'complete', {
|
|
786
|
+
username,
|
|
787
|
+
attempts: logAttempts
|
|
788
|
+
});
|
|
750
789
|
return {};
|
|
751
790
|
} else {
|
|
752
791
|
const token = cuid();
|
|
753
|
-
await self.bearerTokens.
|
|
792
|
+
await self.bearerTokens.insertOne({
|
|
754
793
|
_id: token,
|
|
755
794
|
userId: user._id,
|
|
756
795
|
expires: new Date(new Date().getTime() + (self.options.bearerTokens.lifetime || (86400 * 7 * 2)) * 1000)
|
|
757
796
|
});
|
|
758
797
|
|
|
759
798
|
await self.clearLoginAttempts(user.username);
|
|
799
|
+
self.logInfo(req, 'complete', {
|
|
800
|
+
username,
|
|
801
|
+
attempts: logAttempts
|
|
802
|
+
});
|
|
760
803
|
|
|
761
804
|
return {
|
|
762
805
|
token
|
|
@@ -127,6 +127,10 @@ export default {
|
|
|
127
127
|
checkedCount: {
|
|
128
128
|
type: Number,
|
|
129
129
|
required: true
|
|
130
|
+
},
|
|
131
|
+
moduleName: {
|
|
132
|
+
type: String,
|
|
133
|
+
required: true
|
|
130
134
|
}
|
|
131
135
|
},
|
|
132
136
|
emits: [
|
|
@@ -263,7 +267,16 @@ export default {
|
|
|
263
267
|
async beginGroupedOperation(action, operations) {
|
|
264
268
|
const operation = operations.find(o => o.action === action);
|
|
265
269
|
|
|
266
|
-
await this.confirmOperation(operation);
|
|
270
|
+
operation.modal ? await this.modalOperation(operation) : await this.confirmOperation(operation);
|
|
271
|
+
},
|
|
272
|
+
async modalOperation({
|
|
273
|
+
modal, ...rest
|
|
274
|
+
}) {
|
|
275
|
+
await apos.modal.execute(modal, {
|
|
276
|
+
count: this.checkedCount,
|
|
277
|
+
moduleName: this.moduleName,
|
|
278
|
+
...rest
|
|
279
|
+
});
|
|
267
280
|
},
|
|
268
281
|
async confirmOperation ({
|
|
269
282
|
modalOptions = {}, action, operations, label, ...rest
|
|
@@ -108,7 +108,7 @@ export default {
|
|
|
108
108
|
// 'utility' or 'other', or the entire schema if followedByCategory
|
|
109
109
|
// is falsy
|
|
110
110
|
getFieldsByCategory(followedByCategory) {
|
|
111
|
-
if (followedByCategory) {
|
|
111
|
+
if (followedByCategory && this.utilityFields) {
|
|
112
112
|
return (followedByCategory === 'other')
|
|
113
113
|
? this.schema.filter(field => !this.utilityFields.includes(field.name))
|
|
114
114
|
: this.schema.filter(field => this.utilityFields.includes(field.name));
|
|
@@ -57,6 +57,7 @@ module.exports = {
|
|
|
57
57
|
|
|
58
58
|
self.__helpers = {};
|
|
59
59
|
self.templateData = self.options.templateData || {};
|
|
60
|
+
self.__structuredLoggingEnabled = false;
|
|
60
61
|
|
|
61
62
|
if (self.apos.asset) {
|
|
62
63
|
if (!self.apos.asset.chains) {
|
|
@@ -74,6 +75,11 @@ module.exports = {
|
|
|
74
75
|
// Routes in their final ready-to-add-to-Express form
|
|
75
76
|
self._routes = [];
|
|
76
77
|
|
|
78
|
+
// Enable structured logging after util module is initialized.
|
|
79
|
+
if (self.apos.util && (self.apos.util !== self)) {
|
|
80
|
+
self.__structuredLoggingEnabled = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
// Add i18next phrases if we started up after the i18n module,
|
|
78
84
|
// which will call this for us if we start up before it
|
|
79
85
|
if (self.apos.i18n && (self.apos.i18n !== self)) {
|
|
@@ -89,6 +95,10 @@ module.exports = {
|
|
|
89
95
|
|
|
90
96
|
methods(self) {
|
|
91
97
|
return {
|
|
98
|
+
// `self.logInfo`, `self.logError`, etc. available for every module except
|
|
99
|
+
// `error`, `util` and the `log` module itself.
|
|
100
|
+
...require('./lib/log')(self),
|
|
101
|
+
|
|
92
102
|
compileSectionRoutes(section) {
|
|
93
103
|
_.each(self[section] || {}, function(routes, method) {
|
|
94
104
|
_.each(routes, function(config, name) {
|
|
@@ -231,18 +241,34 @@ module.exports = {
|
|
|
231
241
|
});
|
|
232
242
|
}
|
|
233
243
|
const response = getResponse(err);
|
|
234
|
-
|
|
235
|
-
if (Object.keys(response.data).length > 1) {
|
|
236
|
-
response.fn(`${req.method} ${req.url}: \n\n${err.stack}\n\n${JSON.stringify(response.data, null, ' ')}`);
|
|
237
|
-
} else {
|
|
238
|
-
response.fn(req.method + ' ' + req.url + ': ' + '\n\n' + err.stack);
|
|
239
|
-
}
|
|
244
|
+
logError(req, response, err);
|
|
240
245
|
req.res.status(response.code);
|
|
241
246
|
return req.res.send({
|
|
242
247
|
name: response.name,
|
|
243
248
|
data: response.data,
|
|
244
249
|
message: response.message
|
|
245
250
|
});
|
|
251
|
+
function logError(req, response, error) {
|
|
252
|
+
const typeTrail = response.code === 500 ? '' : `-${response.name}`;
|
|
253
|
+
// Log the actual error, not the message meant for the browser.
|
|
254
|
+
const msg = response.code === 500
|
|
255
|
+
? err.message
|
|
256
|
+
: response.message;
|
|
257
|
+
try {
|
|
258
|
+
self.logError(req, `api-error${typeTrail}`, msg, {
|
|
259
|
+
name: response.name,
|
|
260
|
+
status: response.code,
|
|
261
|
+
stack: error.stack.split('\n').slice(1).map(line => line.trim()),
|
|
262
|
+
errorPath: response.path,
|
|
263
|
+
data: response.data
|
|
264
|
+
});
|
|
265
|
+
} catch (e) {
|
|
266
|
+
// We can't afford to throw here, it would hang the response.
|
|
267
|
+
e.message = 'Structured logging error: ' + e.message;
|
|
268
|
+
// eslint-disable-next-line no-console
|
|
269
|
+
console.error(e);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
246
272
|
function getResponse(err) {
|
|
247
273
|
let name, data, code, fn, message, path;
|
|
248
274
|
if (err && err.name && self.apos.http.errors[err.name]) {
|