@wbce-d9/api 12.6.0 → 12.7.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/dist/auth/drivers/baseoauth.js +18 -7
- package/dist/auth/drivers/baseoauth.test.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/flows.d.ts +14 -0
- package/dist/flows.js +75 -14
- package/dist/flows.test.d.ts +1 -0
- package/dist/logger.js +4 -3
- package/dist/services/fields.js +12 -0
- package/dist/services/graphql/index.js +3 -0
- package/dist/services/payload.js +8 -2
- package/dist/services/specifications.js +2 -1
- package/dist/services/users.js +35 -29
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +10 -2
- package/dist/utils/package.d.ts +1 -1
- package/dist/utils/package.js +14 -2
- package/package.json +7 -7
|
@@ -39,11 +39,16 @@ export class BaseOAuthDriver extends LocalAuthDriver {
|
|
|
39
39
|
provider: this.getConfig()['provider'],
|
|
40
40
|
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
41
41
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
42
|
-
// Update user to update refresh_token and other properties that might have changed
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Update user to update refresh_token and other properties that might have changed.
|
|
43
|
+
// Keep the stored refresh_token if the provider didn't issue a new one (e.g. Google when consent was
|
|
44
|
+
// already granted): refresh() still checks it against the provider
|
|
45
|
+
const updateUserPayload = userPayload.auth_data
|
|
46
|
+
? { ...updatedUserPayload, auth_data: userPayload.auth_data }
|
|
47
|
+
: updatedUserPayload;
|
|
48
|
+
// Skip empty updates, they crash (#127)
|
|
49
|
+
if (Object.keys(updateUserPayload).length > 0) {
|
|
50
|
+
await this.getUserService().updateOne(userId, updateUserPayload);
|
|
51
|
+
}
|
|
47
52
|
return userId;
|
|
48
53
|
}
|
|
49
54
|
// --- NEW USER ----
|
|
@@ -131,10 +136,16 @@ export class BaseOAuthDriver extends LocalAuthDriver {
|
|
|
131
136
|
throw this.handleError(e);
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
else if (this.getConfig()['requireRefreshToken'] === false) {
|
|
140
|
+
// Opt-in: the provider issued no refresh token (e.g. Google when consent was already granted),
|
|
141
|
+
// so the Directus session is the only source of truth for this user
|
|
142
|
+
logger.debug(`[${this.getClientName()}] No refresh token stored, continuing (REQUIRE_REFRESH_TOKEN=false)`);
|
|
143
|
+
}
|
|
134
144
|
else {
|
|
135
|
-
// If no auth connexion, throw user out
|
|
145
|
+
// If no auth connexion, throw user out. On login, the callback retries once with prompt=consent
|
|
146
|
+
// so the provider can issue a refresh_token
|
|
136
147
|
logger.error(`No [${this.getClientName()}] Session found`);
|
|
137
|
-
throw new
|
|
148
|
+
throw new InvalidTokenException();
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
handleError = (e) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/env.js
CHANGED
|
@@ -141,6 +141,7 @@ const allowedEnvironmentVars = [
|
|
|
141
141
|
'AUTH_.+_FIRST_NAME_KEY',
|
|
142
142
|
'AUTH_.+_LAST_NAME_KEY',
|
|
143
143
|
'AUTH_.+_ALLOW_PUBLIC_REGISTRATION',
|
|
144
|
+
'AUTH_.+_REQUIRE_REFRESH_TOKEN',
|
|
144
145
|
'AUTH_.+_DEFAULT_ROLE_ID',
|
|
145
146
|
'AUTH_.+_REGISTER_URL_ALLOW_LIST',
|
|
146
147
|
'AUTH_.+_ICON',
|
package/dist/flows.d.ts
CHANGED
|
@@ -15,8 +15,22 @@ declare class FlowManager {
|
|
|
15
15
|
runOperationFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<unknown>;
|
|
16
16
|
runWebhookFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<unknown>;
|
|
17
17
|
private load;
|
|
18
|
+
/**
|
|
19
|
+
* Manual flows act on behalf of the triggering user: make sure that user can see the flow itself
|
|
20
|
+
* and read the collection / items passed in the payload before running it.
|
|
21
|
+
*/
|
|
22
|
+
private checkManualTriggerAccess;
|
|
18
23
|
private unload;
|
|
19
24
|
private executeFlow;
|
|
25
|
+
/**
|
|
26
|
+
* Return a copy of the event data whose payload has its concealed fields masked, as they would be on read.
|
|
27
|
+
* The data is returned as is when there is nothing to conceal.
|
|
28
|
+
*/
|
|
29
|
+
private concealEventPayload;
|
|
30
|
+
/**
|
|
31
|
+
* Options are resolved against `concealedKeyedData` for the log operation and for the returned options (stored in
|
|
32
|
+
* the flow revisions), and against `keyedData` for every other operation.
|
|
33
|
+
*/
|
|
20
34
|
private executeOperation;
|
|
21
35
|
}
|
|
22
36
|
export {};
|
package/dist/flows.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as sharedExceptions from '@wbce-d9/exceptions';
|
|
|
2
2
|
import { Action } from '@wbce-d9/constants';
|
|
3
3
|
import { applyOptionsData, isValidJSON, parseJSON, toArray } from '@wbce-d9/utils';
|
|
4
4
|
import fastRedact from 'fast-redact';
|
|
5
|
-
import { omit, pick } from 'lodash-es';
|
|
5
|
+
import { cloneDeep, omit, pick } from 'lodash-es';
|
|
6
6
|
import { get } from 'micromustache';
|
|
7
7
|
import { schedule, validate } from 'node-cron';
|
|
8
8
|
import getDatabase from './database/index.js';
|
|
@@ -15,6 +15,7 @@ import { getMessenger } from './messenger.js';
|
|
|
15
15
|
import { ActivityService } from './services/activity.js';
|
|
16
16
|
import * as services from './services/index.js';
|
|
17
17
|
import { FlowsService } from './services/flows.js';
|
|
18
|
+
import { PayloadService } from './services/payload.js';
|
|
18
19
|
import { RevisionsService } from './services/revisions.js';
|
|
19
20
|
import { constructFlowTree } from './utils/construct-flow-tree.js';
|
|
20
21
|
import { getSchema } from './utils/get-schema.js';
|
|
@@ -197,13 +198,16 @@ class FlowManager {
|
|
|
197
198
|
logger.warn(`Specified collection must be one of: ${enabledCollections.join(', ')}.`);
|
|
198
199
|
throw new exceptions.ForbiddenException();
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
201
|
+
const run = () => {
|
|
202
|
+
if (flow.options['async']) {
|
|
203
|
+
this.executeFlow(flow, data, context);
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
return this.executeFlow(flow, data, context);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
return this.checkManualTriggerAccess(flow, targetCollection, data, context).then(run);
|
|
207
211
|
};
|
|
208
212
|
// Default return to $last for manual
|
|
209
213
|
flow.options['return'] = '$last';
|
|
@@ -212,6 +216,34 @@ class FlowManager {
|
|
|
212
216
|
}
|
|
213
217
|
this.isLoaded = true;
|
|
214
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Manual flows act on behalf of the triggering user: make sure that user can see the flow itself
|
|
221
|
+
* and read the collection / items passed in the payload before running it.
|
|
222
|
+
*/
|
|
223
|
+
async checkManualTriggerAccess(flow, collection, data, context) {
|
|
224
|
+
const accountability = context['accountability'];
|
|
225
|
+
if (!accountability)
|
|
226
|
+
throw new exceptions.ForbiddenException();
|
|
227
|
+
if (accountability.admin)
|
|
228
|
+
return;
|
|
229
|
+
const database = context['database'] ?? getDatabase();
|
|
230
|
+
const schema = context['schema'] ?? (await getSchema({ database }));
|
|
231
|
+
const flowsService = new FlowsService({ accountability, knex: database, schema });
|
|
232
|
+
await flowsService.readOne(flow.id, { fields: ['id'] });
|
|
233
|
+
const keys = data?.['body']?.keys;
|
|
234
|
+
if (keys === null || keys === undefined || (Array.isArray(keys) && keys.length === 0)) {
|
|
235
|
+
if (flow.options?.['requireSelection'] !== false)
|
|
236
|
+
throw new exceptions.ForbiddenException();
|
|
237
|
+
const canReadCollection = accountability.permissions?.some((permission) => permission.collection === collection && permission.action === 'read');
|
|
238
|
+
if (!canReadCollection)
|
|
239
|
+
throw new exceptions.ForbiddenException();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!Array.isArray(keys))
|
|
243
|
+
throw new exceptions.ForbiddenException();
|
|
244
|
+
const authorizationService = new services.AuthorizationService({ accountability, knex: database, schema });
|
|
245
|
+
await authorizationService.checkAccess('read', collection, keys);
|
|
246
|
+
}
|
|
215
247
|
async unload() {
|
|
216
248
|
for (const trigger of this.triggerHandlers) {
|
|
217
249
|
trigger.events.forEach((event) => {
|
|
@@ -242,13 +274,22 @@ class FlowManager {
|
|
|
242
274
|
[ACCOUNTABILITY_KEY]: context?.['accountability'] ?? null,
|
|
243
275
|
[ENV_KEY]: pick(env, env['FLOWS_ENV_ALLOW_LIST'] ? toArray(env['FLOWS_ENV_ALLOW_LIST']) : []),
|
|
244
276
|
};
|
|
277
|
+
// Concealed fields of the event payload (e.g. passwords, tokens) must never end up in logs or revisions
|
|
278
|
+
const concealedData = await this.concealEventPayload(flow, data, database, schema);
|
|
279
|
+
const concealedKeyedData = {
|
|
280
|
+
...keyedData,
|
|
281
|
+
[TRIGGER_KEY]: concealedData,
|
|
282
|
+
[LAST_KEY]: concealedData,
|
|
283
|
+
};
|
|
245
284
|
let nextOperation = flow.operation;
|
|
246
285
|
let lastOperationStatus = 'unknown';
|
|
247
286
|
const steps = [];
|
|
248
287
|
while (nextOperation !== null) {
|
|
249
|
-
const { successor, data, status, options } = await this.executeOperation(nextOperation, keyedData, context);
|
|
288
|
+
const { successor, data, status, options } = await this.executeOperation(nextOperation, keyedData, concealedKeyedData, context);
|
|
250
289
|
keyedData[nextOperation.key] = data;
|
|
251
290
|
keyedData[LAST_KEY] = data;
|
|
291
|
+
concealedKeyedData[nextOperation.key] = data;
|
|
292
|
+
concealedKeyedData[LAST_KEY] = data;
|
|
252
293
|
lastOperationStatus = status;
|
|
253
294
|
steps.push({ operation: nextOperation.id, key: nextOperation.key, status, options });
|
|
254
295
|
nextOperation = successor;
|
|
@@ -279,7 +320,7 @@ class FlowManager {
|
|
|
279
320
|
item: flow.id,
|
|
280
321
|
data: {
|
|
281
322
|
steps: steps,
|
|
282
|
-
data: redactLogs(omit(
|
|
323
|
+
data: redactLogs(omit(concealedKeyedData, '$accountability.permissions')), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
|
|
283
324
|
},
|
|
284
325
|
});
|
|
285
326
|
}
|
|
@@ -295,15 +336,35 @@ class FlowManager {
|
|
|
295
336
|
}
|
|
296
337
|
return undefined;
|
|
297
338
|
}
|
|
298
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Return a copy of the event data whose payload has its concealed fields masked, as they would be on read.
|
|
341
|
+
* The data is returned as is when there is nothing to conceal.
|
|
342
|
+
*/
|
|
343
|
+
async concealEventPayload(flow, data, knex, schema) {
|
|
344
|
+
if (flow.trigger !== 'event')
|
|
345
|
+
return data;
|
|
346
|
+
const { collection, payload } = data;
|
|
347
|
+
if (!schema.collections[collection] || !payload)
|
|
348
|
+
return data;
|
|
349
|
+
const payloadService = new PayloadService(collection, { knex, schema });
|
|
350
|
+
const concealedData = cloneDeep(data);
|
|
351
|
+
concealedData.payload = await payloadService.processValues('read', concealedData.payload);
|
|
352
|
+
return concealedData;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Options are resolved against `concealedKeyedData` for the log operation and for the returned options (stored in
|
|
356
|
+
* the flow revisions), and against `keyedData` for every other operation.
|
|
357
|
+
*/
|
|
358
|
+
async executeOperation(operation, keyedData, concealedKeyedData, context = {}) {
|
|
299
359
|
if (!(operation.type in this.operations)) {
|
|
300
360
|
logger.warn(`Couldn't find operation ${operation.type}`);
|
|
301
361
|
return { successor: null, status: 'unknown', data: null, options: null };
|
|
302
362
|
}
|
|
303
363
|
const handler = this.operations[operation.type];
|
|
304
364
|
const options = applyOptionsData(operation.options, keyedData);
|
|
365
|
+
const concealedOptions = applyOptionsData(operation.options, concealedKeyedData);
|
|
305
366
|
try {
|
|
306
|
-
let result = await handler(options, {
|
|
367
|
+
let result = await handler(operation.type === 'log' ? concealedOptions : options, {
|
|
307
368
|
services,
|
|
308
369
|
exceptions: { ...exceptions, ...sharedExceptions },
|
|
309
370
|
env,
|
|
@@ -321,7 +382,7 @@ class FlowManager {
|
|
|
321
382
|
if (typeof result === 'object' && result !== null) {
|
|
322
383
|
result = mapValuesDeep(result, (_, value) => (value === undefined ? null : value));
|
|
323
384
|
}
|
|
324
|
-
return { successor: operation.resolve, status: 'resolve', data: result ?? null, options };
|
|
385
|
+
return { successor: operation.resolve, status: 'resolve', data: result ?? null, options: concealedOptions };
|
|
325
386
|
}
|
|
326
387
|
catch (error) {
|
|
327
388
|
let data;
|
|
@@ -343,7 +404,7 @@ class FlowManager {
|
|
|
343
404
|
successor: operation.reject,
|
|
344
405
|
status: 'reject',
|
|
345
406
|
data,
|
|
346
|
-
options,
|
|
407
|
+
options: concealedOptions,
|
|
347
408
|
};
|
|
348
409
|
}
|
|
349
410
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/logger.js
CHANGED
|
@@ -6,17 +6,18 @@ import { URL } from 'url';
|
|
|
6
6
|
import env from './env.js';
|
|
7
7
|
import { REDACT_TEXT } from './constants.js';
|
|
8
8
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
9
|
+
const REDACTED_PATHS = ['req.headers.authorization', 'req.headers.cookie', 'req.query.access_token'];
|
|
9
10
|
const pinoOptions = {
|
|
10
11
|
level: env['LOG_LEVEL'] || 'info',
|
|
11
12
|
redact: {
|
|
12
|
-
paths:
|
|
13
|
+
paths: REDACTED_PATHS,
|
|
13
14
|
censor: REDACT_TEXT,
|
|
14
15
|
},
|
|
15
16
|
};
|
|
16
17
|
export const httpLoggerOptions = {
|
|
17
18
|
level: env['LOG_LEVEL'] || 'info',
|
|
18
19
|
redact: {
|
|
19
|
-
paths:
|
|
20
|
+
paths: REDACTED_PATHS,
|
|
20
21
|
censor: REDACT_TEXT,
|
|
21
22
|
},
|
|
22
23
|
};
|
|
@@ -43,7 +44,7 @@ if (env['LOG_STYLE'] !== 'raw') {
|
|
|
43
44
|
}
|
|
44
45
|
if (env['LOG_STYLE'] === 'raw') {
|
|
45
46
|
httpLoggerOptions.redact = {
|
|
46
|
-
paths: [
|
|
47
|
+
paths: [...REDACTED_PATHS, 'res.headers'],
|
|
47
48
|
censor: (value, pathParts) => {
|
|
48
49
|
const path = pathParts.join('.');
|
|
49
50
|
if (path === 'res.headers') {
|
package/dist/services/fields.js
CHANGED
|
@@ -458,6 +458,18 @@ export class FieldsService {
|
|
|
458
458
|
if (Object.keys(collectionMetaUpdates).length > 0) {
|
|
459
459
|
await trx('directus_collections').update(collectionMetaUpdates).where({ collection });
|
|
460
460
|
}
|
|
461
|
+
const permissions = await trx('directus_permissions')
|
|
462
|
+
.select('id', 'fields')
|
|
463
|
+
.where({ collection });
|
|
464
|
+
for (const permission of permissions) {
|
|
465
|
+
const permittedFields = permission.fields?.split(',') ?? [];
|
|
466
|
+
if (permittedFields.includes(field) === false)
|
|
467
|
+
continue;
|
|
468
|
+
const remainingFields = permittedFields.filter((permittedField) => permittedField !== field);
|
|
469
|
+
await trx('directus_permissions')
|
|
470
|
+
.update({ fields: remainingFields.length > 0 ? remainingFields.join(',') : null })
|
|
471
|
+
.where({ id: permission.id });
|
|
472
|
+
}
|
|
461
473
|
// Cleanup directus_fields
|
|
462
474
|
const metaRow = await trx
|
|
463
475
|
.select('collection', 'field')
|
|
@@ -104,6 +104,9 @@ export class GraphQLService {
|
|
|
104
104
|
return formattedResult;
|
|
105
105
|
}
|
|
106
106
|
getSchema(type = 'schema') {
|
|
107
|
+
if (type === 'sdl' && env['GRAPHQL_INTROSPECTION'] === false) {
|
|
108
|
+
throw new ForbiddenException();
|
|
109
|
+
}
|
|
107
110
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
108
111
|
const self = this;
|
|
109
112
|
const schemaComposer = new SchemaComposer();
|
package/dist/services/payload.js
CHANGED
|
@@ -3,7 +3,7 @@ import { format, isValid, parseISO } from 'date-fns';
|
|
|
3
3
|
import flat from 'flat';
|
|
4
4
|
import Joi from 'joi';
|
|
5
5
|
import { clone, cloneDeep, isNil, isObject, isPlainObject, omit, pick } from 'lodash-es';
|
|
6
|
-
import { v4 as uuid } from 'uuid';
|
|
6
|
+
import { v4 as uuid, validate as validateUUID } from 'uuid';
|
|
7
7
|
import { parse as wktToGeoJSON } from 'wellknown';
|
|
8
8
|
import { getHelpers } from '../database/helpers/index.js';
|
|
9
9
|
import getDatabase from '../database/index.js';
|
|
@@ -35,13 +35,19 @@ export class PayloadService {
|
|
|
35
35
|
if (action === 'create' || action === 'update') {
|
|
36
36
|
return await generateHash(String(value));
|
|
37
37
|
}
|
|
38
|
+
// Same as conceal
|
|
39
|
+
if (action === 'read')
|
|
40
|
+
return '**********';
|
|
38
41
|
return value;
|
|
39
42
|
},
|
|
40
43
|
async uuid({ action, value }) {
|
|
41
44
|
if (action === 'create' && !value) {
|
|
42
45
|
return uuid();
|
|
43
46
|
}
|
|
44
|
-
|
|
47
|
+
if (action === 'create' && !validateUUID(String(value))) {
|
|
48
|
+
throw new InvalidPayloadException(`Invalid UUID "${value}"`);
|
|
49
|
+
}
|
|
50
|
+
return action === 'create' ? String(value).toLowerCase() : value;
|
|
45
51
|
},
|
|
46
52
|
async 'cast-boolean'({ action, value }) {
|
|
47
53
|
if (action === 'read') {
|
|
@@ -62,7 +62,8 @@ class OASSpecsService {
|
|
|
62
62
|
info: {
|
|
63
63
|
title: 'Dynamic API Specification',
|
|
64
64
|
description: 'This is a dynamically generated API specification for all endpoints existing on the current project.',
|
|
65
|
-
|
|
65
|
+
// Only admins see the real version, as on /server/info
|
|
66
|
+
version: this.accountability?.admin ? version : '0.0.0-hidden',
|
|
66
67
|
},
|
|
67
68
|
servers: [
|
|
68
69
|
{
|
package/dist/services/users.js
CHANGED
|
@@ -5,6 +5,7 @@ import { cloneDeep, isEmpty } from 'lodash-es';
|
|
|
5
5
|
import { performance } from 'perf_hooks';
|
|
6
6
|
import getDatabase from '../database/index.js';
|
|
7
7
|
import env from '../env.js';
|
|
8
|
+
import logger from '../logger.js';
|
|
8
9
|
import { RecordNotUniqueException } from '../exceptions/database/record-not-unique.js';
|
|
9
10
|
import { ForbiddenException, InvalidPayloadException, UnprocessableEntityException } from '../exceptions/index.js';
|
|
10
11
|
import isUrlAllowed from '../utils/is-url-allowed.js';
|
|
@@ -340,37 +341,42 @@ export class UsersService extends ItemsService {
|
|
|
340
341
|
async requestPasswordReset(email, url, subject) {
|
|
341
342
|
const STALL_TIME = 500;
|
|
342
343
|
const timeStart = performance.now();
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
try {
|
|
345
|
+
if (url && isUrlAllowed(url, env['PASSWORD_RESET_URL_ALLOW_LIST']) === false) {
|
|
346
|
+
throw new InvalidPayloadException(`Url "${url}" can't be used to reset passwords.`);
|
|
347
|
+
}
|
|
348
|
+
const user = await this.getUserByEmail(email);
|
|
349
|
+
if (user?.status !== 'active') {
|
|
350
|
+
throw new ForbiddenException();
|
|
351
|
+
}
|
|
352
|
+
const mailService = new MailService({
|
|
353
|
+
schema: this.schema,
|
|
354
|
+
knex: this.knex,
|
|
355
|
+
accountability: this.accountability,
|
|
356
|
+
});
|
|
357
|
+
const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
|
|
358
|
+
const token = jwt.sign(payload, env['SECRET'], { expiresIn: '1d', issuer: 'directus' });
|
|
359
|
+
const acceptURL = url
|
|
360
|
+
? new Url(url).setQuery('token', token).toString()
|
|
361
|
+
: new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
|
|
362
|
+
const subjectLine = subject ? subject : 'Password Reset Request';
|
|
363
|
+
mailService
|
|
364
|
+
.send({
|
|
365
|
+
to: user.email,
|
|
366
|
+
subject: subjectLine,
|
|
367
|
+
template: {
|
|
368
|
+
name: 'password-reset',
|
|
369
|
+
data: {
|
|
370
|
+
url: acceptURL,
|
|
371
|
+
email: user.email,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
.catch((err) => logger.warn(err, `[email] ${err}`));
|
|
347
376
|
}
|
|
348
|
-
|
|
349
|
-
|
|
377
|
+
finally {
|
|
378
|
+
await stall(STALL_TIME, timeStart);
|
|
350
379
|
}
|
|
351
|
-
const mailService = new MailService({
|
|
352
|
-
schema: this.schema,
|
|
353
|
-
knex: this.knex,
|
|
354
|
-
accountability: this.accountability,
|
|
355
|
-
});
|
|
356
|
-
const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
|
|
357
|
-
const token = jwt.sign(payload, env['SECRET'], { expiresIn: '1d', issuer: 'directus' });
|
|
358
|
-
const acceptURL = url
|
|
359
|
-
? new Url(url).setQuery('token', token).toString()
|
|
360
|
-
: new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
|
|
361
|
-
const subjectLine = subject ? subject : 'Password Reset Request';
|
|
362
|
-
await mailService.send({
|
|
363
|
-
to: user.email,
|
|
364
|
-
subject: subjectLine,
|
|
365
|
-
template: {
|
|
366
|
-
name: 'password-reset',
|
|
367
|
-
data: {
|
|
368
|
-
url: acceptURL,
|
|
369
|
-
email: user.email,
|
|
370
|
-
},
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
await stall(STALL_TIME, timeStart);
|
|
374
380
|
}
|
|
375
381
|
async resetPassword(token, password) {
|
|
376
382
|
const { email, scope, hash } = jwt.verify(token, env['SECRET'], { issuer: 'directus' });
|
|
@@ -32,4 +32,4 @@ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuer
|
|
|
32
32
|
};
|
|
33
33
|
export declare function addWhereClauses(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, filter: Filter, collection: string, aliasMap: AliasMap, logical?: 'and' | 'or'): void;
|
|
34
34
|
export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
|
|
35
|
-
export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
|
|
35
|
+
export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
|
|
@@ -33,7 +33,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
|
|
|
33
33
|
dbQuery.groupBy(query.group.map((column) => getColumn(knex, collection, column, false, schema)));
|
|
34
34
|
}
|
|
35
35
|
if (query.aggregate) {
|
|
36
|
-
applyAggregate(dbQuery, query.aggregate, collection);
|
|
36
|
+
applyAggregate(schema, dbQuery, query.aggregate, collection);
|
|
37
37
|
}
|
|
38
38
|
if (query.filter) {
|
|
39
39
|
hasMultiRelationalFilter = applyFilter(knex, schema, dbQuery, query.filter, collection, aliasMap).hasMultiRelationalFilter;
|
|
@@ -166,6 +166,9 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
|
|
|
166
166
|
rootQuery.orderBy(sortRecords);
|
|
167
167
|
return undefined;
|
|
168
168
|
}
|
|
169
|
+
function isConcealedField(schema, collection, field) {
|
|
170
|
+
return schema.collections[collection]?.fields[stripFunction(field)]?.special.includes('conceal') ?? false;
|
|
171
|
+
}
|
|
169
172
|
export function applyLimit(knex, rootQuery, limit) {
|
|
170
173
|
if (typeof limit === 'number') {
|
|
171
174
|
getHelpers(knex).schema.applyLimit(rootQuery, limit);
|
|
@@ -480,6 +483,8 @@ export async function applySearch(schema, dbQuery, searchQuery, collection) {
|
|
|
480
483
|
const fields = Object.entries(schema.collections[collection].fields);
|
|
481
484
|
dbQuery.andWhere(function () {
|
|
482
485
|
fields.forEach(([name, field]) => {
|
|
486
|
+
if (field.special.includes('conceal'))
|
|
487
|
+
return;
|
|
483
488
|
if (['text', 'string'].includes(field.type)) {
|
|
484
489
|
this.orWhereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
|
|
485
490
|
}
|
|
@@ -494,11 +499,14 @@ export async function applySearch(schema, dbQuery, searchQuery, collection) {
|
|
|
494
499
|
});
|
|
495
500
|
});
|
|
496
501
|
}
|
|
497
|
-
export function applyAggregate(dbQuery, aggregate, collection) {
|
|
502
|
+
export function applyAggregate(schema, dbQuery, aggregate, collection) {
|
|
498
503
|
for (const [operation, fields] of Object.entries(aggregate)) {
|
|
499
504
|
if (!fields)
|
|
500
505
|
continue;
|
|
501
506
|
for (const field of fields) {
|
|
507
|
+
if (!operation.startsWith('count') && isConcealedField(schema, collection, field)) {
|
|
508
|
+
throw new InvalidQueryException(`Field with "conceal" special does not allow the "${operation}" aggregate function`);
|
|
509
|
+
}
|
|
502
510
|
if (operation === 'avg') {
|
|
503
511
|
dbQuery.avg(`${collection}.${field}`, { as: `avg->${field}` });
|
|
504
512
|
}
|
package/dist/utils/package.d.ts
CHANGED
package/dist/utils/package.js
CHANGED
|
@@ -4,6 +4,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
5
|
const apiPkgPath = resolve(__dirname, '../../package.json');
|
|
6
6
|
const { name, version } = JSON.parse(readFileSync(apiPkgPath, 'utf8'));
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function pkgVersion(path) {
|
|
8
|
+
try {
|
|
9
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')).version ?? null : null;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// d9 release version (the bundle, matching the Docker image tag): dev-checkout root, then the
|
|
16
|
+
// installed bundle (published/projen image), then the api version as a last resort.
|
|
17
|
+
const mainVersion = pkgVersion(resolve(dirname(apiPkgPath), '../../../../../../package.json')) ??
|
|
18
|
+
pkgVersion(resolve(process.cwd(), 'node_modules/@wbce-d9/directus9/package.json')) ??
|
|
19
|
+
pkgVersion(resolve(process.cwd(), 'node_modules/directus9/package.json')) ??
|
|
20
|
+
version;
|
|
9
21
|
export { name, version, mainVersion };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbce-d9/api",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.7.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -144,20 +144,20 @@
|
|
|
144
144
|
"tmp-promise": "3.0.3",
|
|
145
145
|
"tsx": "3.12.6",
|
|
146
146
|
"uuid-validate": "0.0.3",
|
|
147
|
-
"@wbce-d9/app": "11.0
|
|
148
|
-
"@wbce-d9/exceptions": "10.0.1",
|
|
147
|
+
"@wbce-d9/app": "11.2.0",
|
|
149
148
|
"@wbce-d9/extensions-sdk": "10.0.5",
|
|
150
|
-
"@wbce-d9/specs": "9.26.5",
|
|
151
|
-
"@wbce-d9/constants": "10.0.0",
|
|
152
149
|
"@wbce-d9/schema": "10.0.0",
|
|
150
|
+
"@wbce-d9/specs": "9.26.5",
|
|
153
151
|
"@wbce-d9/storage-driver-azure": "10.0.4",
|
|
154
|
-
"@wbce-d9/storage": "10.0.0",
|
|
155
152
|
"@wbce-d9/storage-driver-cloudinary": "10.0.5",
|
|
153
|
+
"@wbce-d9/storage-driver-local": "10.0.4",
|
|
156
154
|
"@wbce-d9/storage-driver-gcs": "10.0.4",
|
|
157
155
|
"@wbce-d9/storage-driver-s3": "10.0.4",
|
|
158
156
|
"@wbce-d9/update-check": "10.0.4",
|
|
157
|
+
"@wbce-d9/storage": "10.0.0",
|
|
159
158
|
"@wbce-d9/utils": "10.0.4",
|
|
160
|
-
"@wbce-d9/
|
|
159
|
+
"@wbce-d9/constants": "10.0.0",
|
|
160
|
+
"@wbce-d9/exceptions": "10.0.1"
|
|
161
161
|
},
|
|
162
162
|
"devDependencies": {
|
|
163
163
|
"@ngneat/falso": "6.4.0",
|