@wbce-d9/api 10.3.1 → 10.6.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/app.js CHANGED
@@ -108,10 +108,12 @@ export default async function createApp() {
108
108
  await emitter.emitInit('app.before', { app });
109
109
  await emitter.emitInit('middlewares.before', { app });
110
110
  app.use(expressLogger);
111
- app.use((_req, res, next) => {
112
- res.setHeader('X-Powered-By', 'Directus');
113
- next();
114
- });
111
+ if (env['X_POWERED_BY_ENABLED'] === true) {
112
+ app.use((_req, res, next) => {
113
+ res.setHeader('X-Powered-By', 'Directus');
114
+ next();
115
+ });
116
+ }
115
117
  if (env['CORS_ENABLED'] === true) {
116
118
  app.use(cors);
117
119
  }
@@ -3,7 +3,7 @@ import { BaseException } from '@wbce-d9/exceptions';
3
3
  import express, { Router } from 'express';
4
4
  import * as samlify from 'samlify';
5
5
  import { getAuthProvider } from '../../auth.js';
6
- import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
6
+ import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
7
7
  import getDatabase from '../../database/index.js';
8
8
  import emitter from '../../emitter.js';
9
9
  import env from '../../env.js';
@@ -103,13 +103,13 @@ export function createSAMLAuthRouter(providerName) {
103
103
  const { context } = sp.createLogoutRequest(idp, 'redirect', req.body);
104
104
  const authService = new AuthenticationService({ accountability: req.accountability, schema: req.schema });
105
105
  if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
106
- res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_OPTIONS);
106
+ res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
107
107
  }
108
108
  if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
109
109
  const currentRefreshToken = req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
110
110
  if (currentRefreshToken) {
111
111
  await authService.logout(currentRefreshToken);
112
- res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
112
+ res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
113
113
  }
114
114
  }
115
115
  return res.redirect(context);
@@ -230,6 +230,9 @@ REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"
230
230
  # and it should be greater than the time taken for a login request with an invalid password [500]
231
231
  # LOGIN_STALL_TIME=500
232
232
 
233
+ # Whether or not to enable the X-Powered-By Header [true]
234
+ X_POWERED_BY_ENABLED=false
235
+
233
236
  # Whether or not to enable the CORS headers [false]
234
237
  CORS_ENABLED=true
235
238
 
@@ -9,8 +9,10 @@ export declare const COLUMN_TRANSFORMS: string[];
9
9
  export declare const GENERATE_SPECIAL: string[];
10
10
  export declare const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
11
11
  export declare const REFRESH_COOKIE_OPTIONS: CookieOptions;
12
+ export declare const REFRESH_COOKIE_CLEAR_OPTIONS: CookieOptions;
12
13
  export declare const GET_SET_HEADER: (cookieValue: string) => string;
13
14
  export declare const ACCESS_COOKIE_OPTIONS: CookieOptions;
15
+ export declare const ACCESS_COOKIE_CLEAR_OPTIONS: CookieOptions;
14
16
  export declare const OAS_REQUIRED_SCHEMAS: string[];
15
17
  /** Formats from which transformation is supported */
16
18
  export declare const SUPPORTED_IMAGE_TRANSFORM_FORMATS: string[];
package/dist/constants.js CHANGED
@@ -56,6 +56,8 @@ export const REFRESH_COOKIE_OPTIONS = {
56
56
  sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
57
57
  path: '/auth',
58
58
  };
59
+ const { maxAge: refreshCookieMaxAge, expires: refreshCookieExpires, ...refresh_cookie_clear_options } = REFRESH_COOKIE_OPTIONS;
60
+ export const REFRESH_COOKIE_CLEAR_OPTIONS = refresh_cookie_clear_options;
59
61
  export const GET_SET_HEADER = (cookieValue) => {
60
62
  const domainHeader = env['REFRESH_TOKEN_COOKIE_DOMAIN'] ? ` Domain=${env['REFRESH_TOKEN_COOKIE_DOMAIN']};` : '';
61
63
  const partitionedHeader = env['REFRESH_TOKEN_COOKIE_PARTITIONED'] == true ? ' Partitioned;' : '';
@@ -70,6 +72,8 @@ export const ACCESS_COOKIE_OPTIONS = {
70
72
  sameSite: env['ACCESS_TOKEN_COOKIE_SAME_SITE'] || 'strict',
71
73
  path: '/',
72
74
  };
75
+ const { maxAge: accessCookieMaxAge, expires: accessCookieExpires, ...access_cookie_clear_options } = ACCESS_COOKIE_OPTIONS;
76
+ export const ACCESS_COOKIE_CLEAR_OPTIONS = access_cookie_clear_options;
73
77
  export const OAS_REQUIRED_SCHEMAS = ['Diff', 'Schema', 'Query', 'x-metadata'];
74
78
  /** Formats from which transformation is supported */
75
79
  export const SUPPORTED_IMAGE_TRANSFORM_FORMATS = ['image/jpeg', 'image/png', 'image/webp', 'image/tiff', 'image/avif'];
@@ -1,6 +1,6 @@
1
1
  import { Router } from 'express';
2
2
  import { createLDAPAuthRouter, createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter, createSAMLAuthRouter, } from '../auth/drivers/index.js';
3
- import { ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_OPTIONS } from '../constants.js';
3
+ import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../constants.js';
4
4
  import env from '../env.js';
5
5
  import { InvalidPayloadException } from '../exceptions/index.js';
6
6
  import logger from '../logger.js';
@@ -55,6 +55,9 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
55
55
  accountability: accountability,
56
56
  schema: req.schema,
57
57
  });
58
+ if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
59
+ res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
60
+ }
58
61
  const currentRefreshToken = req.body.refresh_token || req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
59
62
  if (!currentRefreshToken) {
60
63
  throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
@@ -95,10 +98,10 @@ router.post('/logout', asyncHandler(async (req, res, next) => {
95
98
  }
96
99
  await authenticationService.logout(currentRefreshToken);
97
100
  if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
98
- res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_OPTIONS);
101
+ res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
99
102
  }
100
103
  if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
101
- res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
104
+ res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
102
105
  }
103
106
  return next();
104
107
  }), respond);
@@ -13,11 +13,15 @@ import asyncHandler from '../utils/async-handler.js';
13
13
  import { generateHash } from '../utils/generate-hash.js';
14
14
  import { sanitizeQuery } from '../utils/sanitize-query.js';
15
15
  const router = Router();
16
+ const randomStringSchemaDesc = Joi.object({
17
+ length: Joi.number().integer().min(1).max(500).default(32),
18
+ });
16
19
  router.get('/random/string', asyncHandler(async (req, res) => {
17
20
  const { nanoid } = await import('nanoid');
18
- if (req.query && req.query['length'] && Number(req.query['length']) > 500)
19
- throw new InvalidQueryException(`"length" can't be more than 500 characters`);
20
- const string = nanoid(req.query?.['length'] ? Number(req.query['length']) : 32);
21
+ const { error, value } = randomStringSchemaDesc.validate(req.query, { allowUnknown: true });
22
+ if (error)
23
+ throw new InvalidQueryException(error.message);
24
+ const string = nanoid(value.length);
21
25
  return res.json({ data: string });
22
26
  }));
23
27
  router.post('/hash/generate', asyncHandler(async (req, res) => {
@@ -35,7 +35,7 @@ export default async function runAST(originalAST, schema, options) {
35
35
  return null;
36
36
  // Run the items through the special transforms
37
37
  const payloadService = new PayloadService(collection, { knex, schema });
38
- let items = await payloadService.processValues('read', rawItems);
38
+ let items = await payloadService.processValues('read', rawItems, query.alias ?? {});
39
39
  if (!items || (Array.isArray(items) && items.length === 0))
40
40
  return items;
41
41
  // Apply the `_in` filters to the nested collection batches
@@ -41,6 +41,9 @@
41
41
  permissions:
42
42
  user:
43
43
  _eq: $CURRENT_USER
44
+ validation:
45
+ user:
46
+ _eq: $CURRENT_USER
44
47
 
45
48
  - collection: directus_presets
46
49
  action: delete
package/dist/env.js CHANGED
@@ -54,6 +54,7 @@ const allowedEnvironmentVars = [
54
54
  'HSTS_.+',
55
55
  // hashing
56
56
  'HASH_.+',
57
+ 'X_POWERED_BY_ENABLED',
57
58
  // cors
58
59
  'CORS_ENABLED',
59
60
  'CORS_ORIGIN',
@@ -226,6 +227,7 @@ const defaults = {
226
227
  LOGIN_STALL_TIME: 500,
227
228
  SERVER_SHUTDOWN_TIMEOUT: 1000,
228
229
  ROOT_REDIRECT: './admin',
230
+ X_POWERED_BY_ENABLED: true,
229
231
  CORS_ENABLED: false,
230
232
  CORS_ORIGIN: false,
231
233
  CORS_METHODS: 'GET,POST,PATCH,DELETE',
package/dist/mailer.js CHANGED
@@ -55,10 +55,18 @@ export default function getMailer() {
55
55
  }));
56
56
  }
57
57
  else if (transportName === 'sendgrid') {
58
- const sg = require('nodemailer-sendgrid');
59
- transporter = nodemailer.createTransport(sg({
60
- apiKey: env['EMAIL_SENDGRID_API_KEY'],
61
- }));
58
+ let auth = false;
59
+ if (env['EMAIL_SENDGRID_API_KEY'] || env['EMAIL_SMTP_PASSWORD']) {
60
+ auth = {
61
+ user: 'apikey',
62
+ pass: env['EMAIL_SENDGRID_API_KEY'],
63
+ };
64
+ }
65
+ transporter = nodemailer.createTransport({
66
+ host: 'smtp.sendgrid.net',
67
+ port: 587,
68
+ auth,
69
+ });
62
70
  }
63
71
  else {
64
72
  logger.warn('Illegal transport given for email. Check the EMAIL_TRANSPORT env var.');
@@ -2,7 +2,7 @@ import { isEqual } from 'lodash-es';
2
2
  import getDatabase from '../database/index.js';
3
3
  import emitter from '../emitter.js';
4
4
  import env from '../env.js';
5
- import { InvalidCredentialsException } from '../exceptions/index.js';
5
+ import { InvalidCredentialsException, TokenExpiredException } from '../exceptions/index.js';
6
6
  import asyncHandler from '../utils/async-handler.js';
7
7
  import { getIPFromReq } from '../utils/get-ip-from-req.js';
8
8
  import isDirectusJWT from '../utils/is-directus-jwt.js';
@@ -40,6 +40,20 @@ export const handler = async (req, _res, next) => {
40
40
  if (req.token) {
41
41
  if (isDirectusJWT(req.token)) {
42
42
  const payload = verifyAccessJWT(req.token, env['SECRET']);
43
+ // Try finding the user with a valid refresh token stored in the access token
44
+ const user = await database
45
+ .select({
46
+ session_expires: 's.expires',
47
+ user_id: 'u.id',
48
+ })
49
+ .from('directus_sessions AS s')
50
+ .leftJoin('directus_users AS u', 's.user', 'u.id')
51
+ .where('s.token', payload.refresh_token)
52
+ .andWhere('s.expires', '>=', new Date())
53
+ .first();
54
+ if (!user) {
55
+ throw new TokenExpiredException();
56
+ }
43
57
  req.accountability.role = payload.role;
44
58
  req.accountability.admin = payload.admin_access === true || payload.admin_access == 1;
45
59
  req.accountability.app = payload.app_access === true || payload.app_access == 1;
@@ -22,7 +22,8 @@ export const respond = asyncHandler(async (req, res) => {
22
22
  cache &&
23
23
  !req.sanitizedQuery.export &&
24
24
  res.locals['cache'] !== false &&
25
- exceedsMaxSize === false) {
25
+ exceedsMaxSize === false &&
26
+ req.originalUrl?.startsWith('/auth')) {
26
27
  const key = getCacheKey(req);
27
28
  try {
28
29
  await setCacheValue(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
@@ -7,7 +7,7 @@ import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
7
7
  import getDatabase from '../database/index.js';
8
8
  import emitter from '../emitter.js';
9
9
  import env from '../env.js';
10
- import { InvalidCredentialsException, InvalidOTPException, InvalidProviderException, UserSuspendedException, } from '../exceptions/index.js';
10
+ import { InvalidCredentialsException, InvalidOTPException, UserSuspendedException } from '../exceptions/index.js';
11
11
  import { createRateLimiter } from '../rate-limiter.js';
12
12
  import { getMilliseconds } from '../utils/get-milliseconds.js';
13
13
  import { stall } from '../utils/stall.js';
@@ -72,20 +72,10 @@ export class AuthenticationService {
72
72
  accountability: this.accountability,
73
73
  });
74
74
  };
75
- if (user?.status !== 'active') {
75
+ if (user?.status !== 'active' || user.provider.toLowerCase() !== providerName.toLowerCase()) {
76
76
  emitStatus('fail');
77
- if (user?.status === 'suspended') {
78
- await stall(STALL_TIME, timeStart);
79
- throw new UserSuspendedException();
80
- }
81
- else {
82
- await stall(STALL_TIME, timeStart);
83
- throw new InvalidCredentialsException();
84
- }
85
- }
86
- else if (user.provider.toLowerCase() !== providerName.toLowerCase()) {
87
77
  await stall(STALL_TIME, timeStart);
88
- throw new InvalidProviderException();
78
+ throw new InvalidCredentialsException();
89
79
  }
90
80
  const settingsService = new SettingsService({
91
81
  knex: this.knex,
@@ -128,11 +118,22 @@ export class AuthenticationService {
128
118
  throw new InvalidOTPException(`"otp" is invalid`);
129
119
  }
130
120
  }
121
+ const refreshToken = nanoid(64);
122
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
123
+ await this.knex('directus_sessions').insert({
124
+ token: refreshToken,
125
+ user: user.id,
126
+ expires: refreshTokenExpiration,
127
+ ip: this.accountability?.ip,
128
+ user_agent: this.accountability?.userAgent,
129
+ origin: this.accountability?.origin,
130
+ });
131
131
  const tokenPayload = {
132
132
  id: user.id,
133
133
  role: user.role,
134
134
  app_access: user.app_access,
135
135
  admin_access: user.admin_access,
136
+ refresh_token: refreshToken,
136
137
  };
137
138
  const customClaims = await emitter.emitFilter('auth.jwt', tokenPayload, {
138
139
  status: 'pending',
@@ -148,16 +149,6 @@ export class AuthenticationService {
148
149
  expiresIn: env['ACCESS_TOKEN_TTL'],
149
150
  issuer: 'directus',
150
151
  });
151
- const refreshToken = nanoid(64);
152
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
153
- await this.knex('directus_sessions').insert({
154
- token: refreshToken,
155
- user: user.id,
156
- expires: refreshTokenExpiration,
157
- ip: this.accountability?.ip,
158
- user_agent: this.accountability?.userAgent,
159
- origin: this.accountability?.origin,
160
- });
161
152
  await this.knex('directus_sessions').delete().where('expires', '<', new Date());
162
153
  if (this.accountability) {
163
154
  await this.activityService.createOne({
@@ -260,11 +251,20 @@ export class AuthenticationService {
260
251
  admin_access: record.role_admin_access,
261
252
  });
262
253
  }
254
+ const newRefreshToken = nanoid(64);
255
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
256
+ await this.knex('directus_sessions')
257
+ .update({
258
+ token: newRefreshToken,
259
+ expires: refreshTokenExpiration,
260
+ })
261
+ .where({ token: refreshToken });
263
262
  const tokenPayload = {
264
263
  id: record.user_id,
265
264
  role: record.role_id,
266
265
  app_access: record.role_app_access,
267
266
  admin_access: record.role_admin_access,
267
+ refresh_token: newRefreshToken,
268
268
  };
269
269
  if (record.share_id) {
270
270
  tokenPayload.share = record.share_id;
@@ -291,14 +291,6 @@ export class AuthenticationService {
291
291
  expiresIn: env['ACCESS_TOKEN_TTL'],
292
292
  issuer: 'directus',
293
293
  });
294
- const newRefreshToken = nanoid(64);
295
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
296
- await this.knex('directus_sessions')
297
- .update({
298
- token: newRefreshToken,
299
- expires: refreshTokenExpiration,
300
- })
301
- .where({ token: refreshToken });
302
294
  if (record.user_id) {
303
295
  await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.user_id });
304
296
  }
@@ -5,7 +5,7 @@ import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID,
5
5
  import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
6
6
  import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
7
7
  import { clearSystemCache, getCache } from '../../cache.js';
8
- import { ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
8
+ import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
9
9
  import getDatabase from '../../database/index.js';
10
10
  import env from '../../env.js';
11
11
  import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../../exceptions/index.js';
@@ -1766,10 +1766,10 @@ export class GraphQLService {
1766
1766
  }
1767
1767
  await authenticationService.logout(currentRefreshToken);
1768
1768
  if (req?.cookies?.[env['ACCESS_TOKEN_COOKIE_NAME']]) {
1769
- res?.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_OPTIONS);
1769
+ res?.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
1770
1770
  }
1771
1771
  if (req?.cookies?.[env['REFRESH_TOKEN_COOKIE_NAME']]) {
1772
- res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
1772
+ res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
1773
1773
  }
1774
1774
  return true;
1775
1775
  },
@@ -1894,10 +1894,10 @@ export class GraphQLService {
1894
1894
  },
1895
1895
  resolve: async (_, args) => {
1896
1896
  const { nanoid } = await import('nanoid');
1897
- if (args['length'] && Number(args['length']) > 500) {
1898
- throw new InvalidPayloadException(`"length" can't be more than 500 characters`);
1897
+ if (args['length'] !== undefined && (args['length'] < 1 || args['length'] > 500)) {
1898
+ throw new InvalidPayloadException(`"length" must be between 1 and 500`);
1899
1899
  }
1900
- return nanoid(args['length'] ? Number(args['length']) : 32);
1900
+ return nanoid(args['length'] ? args['length'] : 32);
1901
1901
  },
1902
1902
  },
1903
1903
  utils_hash_generate: {
@@ -27,6 +27,8 @@ export declare class PayloadService {
27
27
  transformers: Transformers;
28
28
  processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
29
29
  processValues(action: Action, payload: Partial<Item>): Promise<Partial<Item>>;
30
+ processValues(action: Action, payloads: Partial<Item>[], aliasMap: Record<string, string>): Promise<Partial<Item>[]>;
31
+ processValues(action: Action, payload: Partial<Item>, aliasMap: Record<string, string>): Promise<Partial<Item>>;
30
32
  processAggregates(payload: Partial<Item>[]): void;
31
33
  processField(field: SchemaOverview['collections'][string]['fields'][string], payload: Partial<Item>, action: Action, accountability: Accountability | null): Promise<any>;
32
34
  /**
@@ -121,12 +121,20 @@ export class PayloadService {
121
121
  return value;
122
122
  },
123
123
  };
124
- async processValues(action, payload) {
124
+ async processValues(action, payload, aliasMap = {}) {
125
125
  const processedPayload = toArray(payload);
126
126
  if (processedPayload.length === 0)
127
127
  return [];
128
128
  const fieldsInPayload = Object.keys(processedPayload[0]);
129
129
  let specialFieldsInCollection = Object.entries(this.schema.collections[this.collection].fields).filter(([_name, field]) => field.special && field.special.length > 0);
130
+ const aliasEntries = Object.entries(aliasMap);
131
+ for (const [name, field] of specialFieldsInCollection) {
132
+ for (const [aliasName, fieldName] of aliasEntries) {
133
+ if (fieldName === name) {
134
+ specialFieldsInCollection.push([aliasName, { ...field, field: aliasName }]);
135
+ }
136
+ }
137
+ }
130
138
  if (action === 'read') {
131
139
  specialFieldsInCollection = specialFieldsInCollection.filter(([name]) => {
132
140
  return fieldsInPayload.includes(name);
@@ -59,6 +59,17 @@ export class SharesService extends ItemsService {
59
59
  await this.knex('directus_shares')
60
60
  .update({ times_used: record.share_times_used + 1 })
61
61
  .where('id', record.share_id);
62
+ const refreshToken = nanoid(64);
63
+ const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
64
+ await this.knex('directus_sessions').insert({
65
+ token: refreshToken,
66
+ expires: refreshTokenExpiration,
67
+ ip: this.accountability?.ip,
68
+ user_agent: this.accountability?.userAgent,
69
+ origin: this.accountability?.origin,
70
+ share: record.share_id,
71
+ });
72
+ await this.knex('directus_sessions').delete().where('expires', '<', new Date());
62
73
  const tokenPayload = {
63
74
  app_access: false,
64
75
  admin_access: false,
@@ -68,22 +79,12 @@ export class SharesService extends ItemsService {
68
79
  item: record.share_item,
69
80
  collection: record.share_collection,
70
81
  },
82
+ refresh_token: refreshToken,
71
83
  };
72
84
  const accessToken = jwt.sign(tokenPayload, env['SECRET'], {
73
85
  expiresIn: env['ACCESS_TOKEN_TTL'],
74
86
  issuer: 'directus',
75
87
  });
76
- const refreshToken = nanoid(64);
77
- const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
78
- await this.knex('directus_sessions').insert({
79
- token: refreshToken,
80
- expires: refreshTokenExpiration,
81
- ip: this.accountability?.ip,
82
- user_agent: this.accountability?.userAgent,
83
- origin: this.accountability?.origin,
84
- share: record.share_id,
85
- });
86
- await this.knex('directus_sessions').delete().where('expires', '<', new Date());
87
88
  return {
88
89
  accessToken,
89
90
  refreshToken,
@@ -117,7 +117,7 @@ export class UsersService extends ItemsService {
117
117
  */
118
118
  async getUserByEmail(email) {
119
119
  return await this.knex
120
- .select('id', 'role', 'status', 'password')
120
+ .select('id', 'role', 'status', 'password', 'email')
121
121
  .from('directus_users')
122
122
  .whereRaw(`LOWER(??) = ?`, ['email', email.toLowerCase()])
123
123
  .first();
@@ -307,13 +307,13 @@ export class UsersService extends ItemsService {
307
307
  if (isEmpty(user) || user.status === 'invited') {
308
308
  const subjectLine = subject ?? "You've been invited";
309
309
  await mailService.send({
310
- to: email,
310
+ to: user.email,
311
311
  subject: subjectLine,
312
312
  template: {
313
313
  name: 'user-invitation',
314
314
  data: {
315
315
  url: this.inviteUrl(email, url),
316
- email,
316
+ email: user.email,
317
317
  },
318
318
  },
319
319
  });
@@ -351,20 +351,20 @@ export class UsersService extends ItemsService {
351
351
  knex: this.knex,
352
352
  accountability: this.accountability,
353
353
  });
354
- const payload = { email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
354
+ const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
355
355
  const token = jwt.sign(payload, env['SECRET'], { expiresIn: '1d', issuer: 'directus' });
356
356
  const acceptURL = url
357
357
  ? new Url(url).setQuery('token', token).toString()
358
358
  : new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
359
359
  const subjectLine = subject ? subject : 'Password Reset Request';
360
360
  await mailService.send({
361
- to: email,
361
+ to: user.email,
362
362
  subject: subjectLine,
363
363
  template: {
364
364
  name: 'password-reset',
365
365
  data: {
366
366
  url: acceptURL,
367
- email,
367
+ email: user.email,
368
368
  },
369
369
  },
370
370
  });
@@ -34,6 +34,7 @@ export type DirectusTokenPayload = {
34
34
  collection: string;
35
35
  item: string;
36
36
  };
37
+ refresh_token: string;
37
38
  };
38
39
  export type ShareData = {
39
40
  share_id: string;
package/dist/utils/jwt.js CHANGED
@@ -21,9 +21,9 @@ export function verifyJWT(token, secret) {
21
21
  return payload;
22
22
  }
23
23
  export function verifyAccessJWT(token, secret) {
24
- const { id, role, app_access, admin_access, share, share_scope } = verifyJWT(token, secret);
25
- if (role === undefined || app_access === undefined || admin_access === undefined) {
24
+ const { id, role, app_access, admin_access, share, share_scope, refresh_token } = verifyJWT(token, secret);
25
+ if (role === undefined || app_access === undefined || admin_access === undefined || refresh_token === undefined) {
26
26
  throw new InvalidTokenException('Invalid token payload.');
27
27
  }
28
- return { id, role, app_access, admin_access, share, share_scope };
28
+ return { id, role, app_access, admin_access, share, share_scope, refresh_token };
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbce-d9/api",
3
- "version": "10.3.1",
3
+ "version": "10.6.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": [
@@ -72,7 +72,7 @@
72
72
  "@wbce-d9/format-title": "10.0.0",
73
73
  "argon2": "0.30.3",
74
74
  "async": "3.2.4",
75
- "axios": "^1.7.2",
75
+ "axios": "^1.8.2",
76
76
  "busboy": "1.6.0",
77
77
  "bytes": "3.1.2",
78
78
  "camelcase": "7.0.1",
@@ -80,7 +80,7 @@
80
80
  "chokidar": "3.5.3",
81
81
  "commander": "10.0.1",
82
82
  "content-disposition": "0.5.4",
83
- "cookie-parser": "1.4.6",
83
+ "cookie-parser": "1.4.7",
84
84
  "cors": "2.8.5",
85
85
  "csv-parser": "3.0.0",
86
86
  "date-fns": "2.29.3",
@@ -91,11 +91,11 @@
91
91
  "eventemitter2": "6.4.9",
92
92
  "execa": "7.1.1",
93
93
  "exif-reader": "1.2.0",
94
- "express": "4.18.2",
94
+ "express": "^4.21.2",
95
95
  "fast-redact": "3.1.2",
96
96
  "flat": "5.0.2",
97
97
  "fs-extra": "11.1.1",
98
- "graphql": "16.6.0",
98
+ "graphql": "16.8.1",
99
99
  "graphql-compose": "9.0.10",
100
100
  "helmet": "6.1.5",
101
101
  "icc": "3.0.0",
@@ -107,7 +107,7 @@
107
107
  "json2csv": "5.0.7",
108
108
  "jsonwebtoken": "9.0.0",
109
109
  "keyv": "4.5.2",
110
- "knex": "2.4.2",
110
+ "knex": "3.1.0",
111
111
  "ldapjs": "2.3.3",
112
112
  "liquidjs": "10.7.0",
113
113
  "lodash-es": "4.17.21",
@@ -115,10 +115,10 @@
115
115
  "micromustache": "8.0.3",
116
116
  "mime-types": "2.1.35",
117
117
  "ms": "2.1.3",
118
- "nanoid": "4.0.2",
118
+ "nanoid": "5.0.9",
119
119
  "node-cron": "3.0.2",
120
120
  "node-machine-id": "1.1.12",
121
- "nodemailer": "6.9.1",
121
+ "nodemailer": "6.9.9",
122
122
  "object-hash": "3.0.0",
123
123
  "openapi3-ts": "3.2.0",
124
124
  "openid-client": "5.4.0",
@@ -130,10 +130,10 @@
130
130
  "pino-pretty": "10.0.0",
131
131
  "qs": "6.11.1",
132
132
  "rate-limiter-flexible": "2.4.1",
133
- "rollup": "3.20.7",
133
+ "rollup": "3.29.5",
134
134
  "samlify": "2.8.10",
135
- "sanitize-html": "2.10.0",
136
- "sharp": "0.32.0",
135
+ "sanitize-html": "2.12.1",
136
+ "sharp": "0.32.6",
137
137
  "snappy": "7.2.2",
138
138
  "stream-json": "1.7.5",
139
139
  "strip-bom-stream": "5.0.0",
@@ -143,20 +143,20 @@
143
143
  "uuid-validate": "0.0.3",
144
144
  "vm2": "^3.9.19",
145
145
  "wellknown": "0.5.0",
146
- "@wbce-d9/app": "9.29.1",
147
- "@wbce-d9/constants": "9.26.4",
148
- "@wbce-d9/extensions-sdk": "9.26.6",
149
- "@wbce-d9/specs": "9.26.4",
146
+ "@wbce-d9/app": "9.32.0",
147
+ "@wbce-d9/constants": "9.28.0",
148
+ "@wbce-d9/extensions-sdk": "9.27.1",
150
149
  "@wbce-d9/exceptions": "9.26.4",
151
- "@wbce-d9/storage": "9.26.4",
152
150
  "@wbce-d9/schema": "9.26.4",
153
- "@wbce-d9/storage-driver-azure": "9.26.6",
154
- "@wbce-d9/storage-driver-cloudinary": "9.26.6",
155
- "@wbce-d9/storage-driver-gcs": "9.26.6",
156
- "@wbce-d9/storage-driver-local": "9.26.6",
157
- "@wbce-d9/storage-driver-s3": "9.26.8",
158
- "@wbce-d9/update-check": "9.26.4",
159
- "@wbce-d9/utils": "9.28.1"
151
+ "@wbce-d9/storage-driver-azure": "9.26.8",
152
+ "@wbce-d9/specs": "9.26.4",
153
+ "@wbce-d9/storage": "9.26.4",
154
+ "@wbce-d9/storage-driver-cloudinary": "9.27.0",
155
+ "@wbce-d9/storage-driver-local": "9.26.8",
156
+ "@wbce-d9/storage-driver-s3": "9.26.10",
157
+ "@wbce-d9/storage-driver-gcs": "9.26.8",
158
+ "@wbce-d9/utils": "9.29.1",
159
+ "@wbce-d9/update-check": "9.28.0"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@ngneat/falso": "6.4.0",
@@ -200,11 +200,11 @@
200
200
  "@wbce-d9/tsconfig": "0.0.7",
201
201
  "copyfiles": "2.4.1",
202
202
  "form-data": "4.0.0",
203
- "knex-mock-client": "2.0.0",
203
+ "knex-mock-client": "3.0.2",
204
204
  "supertest": "6.3.3",
205
205
  "typescript": "5.0.4",
206
206
  "vitest": "0.30.1",
207
- "@wbce-d9/types": "9.26.4"
207
+ "@wbce-d9/types": "9.28.0"
208
208
  },
209
209
  "optionalDependencies": {
210
210
  "@keyv/redis": "2.5.7",
@@ -212,10 +212,9 @@
212
212
  "memcached": "2.2.2",
213
213
  "mysql": "2.18.1",
214
214
  "nodemailer-mailgun-transport": "2.1.5",
215
- "nodemailer-sendgrid": "1.0.3",
216
215
  "pg": "8.10.0",
217
216
  "sqlite3": "5.1.6",
218
- "tedious": "16.0.0"
217
+ "tedious": "18.6.1"
219
218
  },
220
219
  "engines": {
221
220
  "node": ">=18.0.0"