@appweaver/core 1.5.0 → 1.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.
@@ -23,7 +23,6 @@ export type OneTimeTokenMinAggregateOutputType = {
23
23
  tokenHash: string | null;
24
24
  purpose: string | null;
25
25
  expiresAt: Date | null;
26
- updatedAt: Date | null;
27
26
  createdAt: Date | null;
28
27
  };
29
28
  export type OneTimeTokenMaxAggregateOutputType = {
@@ -31,16 +30,14 @@ export type OneTimeTokenMaxAggregateOutputType = {
31
30
  tokenHash: string | null;
32
31
  purpose: string | null;
33
32
  expiresAt: Date | null;
34
- updatedAt: Date | null;
35
33
  createdAt: Date | null;
36
34
  };
37
35
  export type OneTimeTokenCountAggregateOutputType = {
38
36
  id: number;
39
37
  tokenHash: number;
40
38
  purpose: number;
41
- expiresAt: number;
42
39
  data: number;
43
- updatedAt: number;
40
+ expiresAt: number;
44
41
  createdAt: number;
45
42
  _all: number;
46
43
  };
@@ -55,7 +52,6 @@ export type OneTimeTokenMinAggregateInputType = {
55
52
  tokenHash?: true;
56
53
  purpose?: true;
57
54
  expiresAt?: true;
58
- updatedAt?: true;
59
55
  createdAt?: true;
60
56
  };
61
57
  export type OneTimeTokenMaxAggregateInputType = {
@@ -63,16 +59,14 @@ export type OneTimeTokenMaxAggregateInputType = {
63
59
  tokenHash?: true;
64
60
  purpose?: true;
65
61
  expiresAt?: true;
66
- updatedAt?: true;
67
62
  createdAt?: true;
68
63
  };
69
64
  export type OneTimeTokenCountAggregateInputType = {
70
65
  id?: true;
71
66
  tokenHash?: true;
72
67
  purpose?: true;
73
- expiresAt?: true;
74
68
  data?: true;
75
- updatedAt?: true;
69
+ expiresAt?: true;
76
70
  createdAt?: true;
77
71
  _all?: true;
78
72
  };
@@ -156,9 +150,8 @@ export type OneTimeTokenGroupByOutputType = {
156
150
  id: number;
157
151
  tokenHash: string;
158
152
  purpose: string;
159
- expiresAt: Date;
160
153
  data: runtime.JsonValue;
161
- updatedAt: Date;
154
+ expiresAt: Date;
162
155
  createdAt: Date;
163
156
  _count: OneTimeTokenCountAggregateOutputType | null;
164
157
  _avg: OneTimeTokenAvgAggregateOutputType | null;
@@ -176,39 +169,35 @@ export type OneTimeTokenWhereInput = {
176
169
  id?: Prisma.IntFilter<"OneTimeToken"> | number;
177
170
  tokenHash?: Prisma.StringFilter<"OneTimeToken"> | string;
178
171
  purpose?: Prisma.StringFilter<"OneTimeToken"> | string;
179
- expiresAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
180
172
  data?: Prisma.JsonFilter<"OneTimeToken">;
181
- updatedAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
173
+ expiresAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
182
174
  createdAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
183
175
  };
184
176
  export type OneTimeTokenOrderByWithRelationInput = {
185
177
  id?: Prisma.SortOrder;
186
178
  tokenHash?: Prisma.SortOrder;
187
179
  purpose?: Prisma.SortOrder;
188
- expiresAt?: Prisma.SortOrder;
189
180
  data?: Prisma.SortOrder;
190
- updatedAt?: Prisma.SortOrder;
181
+ expiresAt?: Prisma.SortOrder;
191
182
  createdAt?: Prisma.SortOrder;
192
183
  };
193
184
  export type OneTimeTokenWhereUniqueInput = Prisma.AtLeast<{
194
185
  id?: number;
186
+ tokenHash?: string;
195
187
  AND?: Prisma.OneTimeTokenWhereInput | Prisma.OneTimeTokenWhereInput[];
196
188
  OR?: Prisma.OneTimeTokenWhereInput[];
197
189
  NOT?: Prisma.OneTimeTokenWhereInput | Prisma.OneTimeTokenWhereInput[];
198
- tokenHash?: Prisma.StringFilter<"OneTimeToken"> | string;
199
190
  purpose?: Prisma.StringFilter<"OneTimeToken"> | string;
200
- expiresAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
201
191
  data?: Prisma.JsonFilter<"OneTimeToken">;
202
- updatedAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
192
+ expiresAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
203
193
  createdAt?: Prisma.DateTimeFilter<"OneTimeToken"> | Date | string;
204
- }, "id">;
194
+ }, "id" | "tokenHash">;
205
195
  export type OneTimeTokenOrderByWithAggregationInput = {
206
196
  id?: Prisma.SortOrder;
207
197
  tokenHash?: Prisma.SortOrder;
208
198
  purpose?: Prisma.SortOrder;
209
- expiresAt?: Prisma.SortOrder;
210
199
  data?: Prisma.SortOrder;
211
- updatedAt?: Prisma.SortOrder;
200
+ expiresAt?: Prisma.SortOrder;
212
201
  createdAt?: Prisma.SortOrder;
213
202
  _count?: Prisma.OneTimeTokenCountOrderByAggregateInput;
214
203
  _avg?: Prisma.OneTimeTokenAvgOrderByAggregateInput;
@@ -223,78 +212,69 @@ export type OneTimeTokenScalarWhereWithAggregatesInput = {
223
212
  id?: Prisma.IntWithAggregatesFilter<"OneTimeToken"> | number;
224
213
  tokenHash?: Prisma.StringWithAggregatesFilter<"OneTimeToken"> | string;
225
214
  purpose?: Prisma.StringWithAggregatesFilter<"OneTimeToken"> | string;
226
- expiresAt?: Prisma.DateTimeWithAggregatesFilter<"OneTimeToken"> | Date | string;
227
215
  data?: Prisma.JsonWithAggregatesFilter<"OneTimeToken">;
228
- updatedAt?: Prisma.DateTimeWithAggregatesFilter<"OneTimeToken"> | Date | string;
216
+ expiresAt?: Prisma.DateTimeWithAggregatesFilter<"OneTimeToken"> | Date | string;
229
217
  createdAt?: Prisma.DateTimeWithAggregatesFilter<"OneTimeToken"> | Date | string;
230
218
  };
231
219
  export type OneTimeTokenCreateInput = {
232
220
  tokenHash: string;
233
221
  purpose: string;
234
- expiresAt: Date | string;
235
222
  data: Prisma.JsonNullValueInput | runtime.InputJsonValue;
236
- updatedAt?: Date | string;
223
+ expiresAt: Date | string;
237
224
  createdAt?: Date | string;
238
225
  };
239
226
  export type OneTimeTokenUncheckedCreateInput = {
240
227
  id?: number;
241
228
  tokenHash: string;
242
229
  purpose: string;
243
- expiresAt: Date | string;
244
230
  data: Prisma.JsonNullValueInput | runtime.InputJsonValue;
245
- updatedAt?: Date | string;
231
+ expiresAt: Date | string;
246
232
  createdAt?: Date | string;
247
233
  };
248
234
  export type OneTimeTokenUpdateInput = {
249
235
  tokenHash?: Prisma.StringFieldUpdateOperationsInput | string;
250
236
  purpose?: Prisma.StringFieldUpdateOperationsInput | string;
251
- expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
252
237
  data?: Prisma.JsonNullValueInput | runtime.InputJsonValue;
253
- updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
238
+ expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
254
239
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
255
240
  };
256
241
  export type OneTimeTokenUncheckedUpdateInput = {
257
242
  id?: Prisma.IntFieldUpdateOperationsInput | number;
258
243
  tokenHash?: Prisma.StringFieldUpdateOperationsInput | string;
259
244
  purpose?: Prisma.StringFieldUpdateOperationsInput | string;
260
- expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
261
245
  data?: Prisma.JsonNullValueInput | runtime.InputJsonValue;
262
- updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
246
+ expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
263
247
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
264
248
  };
265
249
  export type OneTimeTokenCreateManyInput = {
266
250
  id?: number;
267
251
  tokenHash: string;
268
252
  purpose: string;
269
- expiresAt: Date | string;
270
253
  data: Prisma.JsonNullValueInput | runtime.InputJsonValue;
271
- updatedAt?: Date | string;
254
+ expiresAt: Date | string;
272
255
  createdAt?: Date | string;
273
256
  };
274
257
  export type OneTimeTokenUpdateManyMutationInput = {
275
258
  tokenHash?: Prisma.StringFieldUpdateOperationsInput | string;
276
259
  purpose?: Prisma.StringFieldUpdateOperationsInput | string;
277
- expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
278
260
  data?: Prisma.JsonNullValueInput | runtime.InputJsonValue;
279
- updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
261
+ expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
280
262
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
281
263
  };
282
264
  export type OneTimeTokenUncheckedUpdateManyInput = {
283
265
  id?: Prisma.IntFieldUpdateOperationsInput | number;
284
266
  tokenHash?: Prisma.StringFieldUpdateOperationsInput | string;
285
267
  purpose?: Prisma.StringFieldUpdateOperationsInput | string;
286
- expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
287
268
  data?: Prisma.JsonNullValueInput | runtime.InputJsonValue;
288
- updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
269
+ expiresAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
289
270
  createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string;
290
271
  };
291
272
  export type OneTimeTokenCountOrderByAggregateInput = {
292
273
  id?: Prisma.SortOrder;
293
274
  tokenHash?: Prisma.SortOrder;
294
275
  purpose?: Prisma.SortOrder;
295
- expiresAt?: Prisma.SortOrder;
296
276
  data?: Prisma.SortOrder;
297
- updatedAt?: Prisma.SortOrder;
277
+ expiresAt?: Prisma.SortOrder;
298
278
  createdAt?: Prisma.SortOrder;
299
279
  };
300
280
  export type OneTimeTokenAvgOrderByAggregateInput = {
@@ -305,7 +285,6 @@ export type OneTimeTokenMaxOrderByAggregateInput = {
305
285
  tokenHash?: Prisma.SortOrder;
306
286
  purpose?: Prisma.SortOrder;
307
287
  expiresAt?: Prisma.SortOrder;
308
- updatedAt?: Prisma.SortOrder;
309
288
  createdAt?: Prisma.SortOrder;
310
289
  };
311
290
  export type OneTimeTokenMinOrderByAggregateInput = {
@@ -313,7 +292,6 @@ export type OneTimeTokenMinOrderByAggregateInput = {
313
292
  tokenHash?: Prisma.SortOrder;
314
293
  purpose?: Prisma.SortOrder;
315
294
  expiresAt?: Prisma.SortOrder;
316
- updatedAt?: Prisma.SortOrder;
317
295
  createdAt?: Prisma.SortOrder;
318
296
  };
319
297
  export type OneTimeTokenSumOrderByAggregateInput = {
@@ -323,39 +301,35 @@ export type OneTimeTokenSelect<ExtArgs extends runtime.Types.Extensions.Internal
323
301
  id?: boolean;
324
302
  tokenHash?: boolean;
325
303
  purpose?: boolean;
326
- expiresAt?: boolean;
327
304
  data?: boolean;
328
- updatedAt?: boolean;
305
+ expiresAt?: boolean;
329
306
  createdAt?: boolean;
330
307
  }, ExtArgs["result"]["oneTimeToken"]>;
331
308
  export type OneTimeTokenSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
332
309
  id?: boolean;
333
310
  tokenHash?: boolean;
334
311
  purpose?: boolean;
335
- expiresAt?: boolean;
336
312
  data?: boolean;
337
- updatedAt?: boolean;
313
+ expiresAt?: boolean;
338
314
  createdAt?: boolean;
339
315
  }, ExtArgs["result"]["oneTimeToken"]>;
340
316
  export type OneTimeTokenSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
341
317
  id?: boolean;
342
318
  tokenHash?: boolean;
343
319
  purpose?: boolean;
344
- expiresAt?: boolean;
345
320
  data?: boolean;
346
- updatedAt?: boolean;
321
+ expiresAt?: boolean;
347
322
  createdAt?: boolean;
348
323
  }, ExtArgs["result"]["oneTimeToken"]>;
349
324
  export type OneTimeTokenSelectScalar = {
350
325
  id?: boolean;
351
326
  tokenHash?: boolean;
352
327
  purpose?: boolean;
353
- expiresAt?: boolean;
354
328
  data?: boolean;
355
- updatedAt?: boolean;
329
+ expiresAt?: boolean;
356
330
  createdAt?: boolean;
357
331
  };
358
- export type OneTimeTokenOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "tokenHash" | "purpose" | "expiresAt" | "data" | "updatedAt" | "createdAt", ExtArgs["result"]["oneTimeToken"]>;
332
+ export type OneTimeTokenOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "tokenHash" | "purpose" | "data" | "expiresAt" | "createdAt", ExtArgs["result"]["oneTimeToken"]>;
359
333
  export type $OneTimeTokenPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
360
334
  name: "OneTimeToken";
361
335
  objects: {};
@@ -363,12 +337,11 @@ export type $OneTimeTokenPayload<ExtArgs extends runtime.Types.Extensions.Intern
363
337
  id: number;
364
338
  tokenHash: string;
365
339
  purpose: string;
366
- expiresAt: Date;
367
340
  data: runtime.JsonValue;
341
+ expiresAt: Date;
368
342
  /**
369
343
  * Audit columns
370
344
  */
371
- updatedAt: Date;
372
345
  createdAt: Date;
373
346
  }, ExtArgs["result"]["oneTimeToken"]>;
374
347
  composites: {};
@@ -727,9 +700,8 @@ export interface OneTimeTokenFieldRefs {
727
700
  readonly id: Prisma.FieldRef<"OneTimeToken", 'Int'>;
728
701
  readonly tokenHash: Prisma.FieldRef<"OneTimeToken", 'String'>;
729
702
  readonly purpose: Prisma.FieldRef<"OneTimeToken", 'String'>;
730
- readonly expiresAt: Prisma.FieldRef<"OneTimeToken", 'DateTime'>;
731
703
  readonly data: Prisma.FieldRef<"OneTimeToken", 'Json'>;
732
- readonly updatedAt: Prisma.FieldRef<"OneTimeToken", 'DateTime'>;
704
+ readonly expiresAt: Prisma.FieldRef<"OneTimeToken", 'DateTime'>;
733
705
  readonly createdAt: Prisma.FieldRef<"OneTimeToken", 'DateTime'>;
734
706
  }
735
707
  /**
@@ -7,8 +7,11 @@ const context_1 = require("../context");
7
7
  class BullQueue extends common_1.Queue {
8
8
  /** @internal */
9
9
  _queues = new Map();
10
+ /** @internal */
11
+ _healthCheckQueue;
10
12
  async onDestroy() {
11
13
  await this.closeAll();
14
+ await this._healthCheckQueue?.close();
12
15
  }
13
16
  get(name) {
14
17
  let processor = this._queues.get(name);
@@ -33,9 +36,10 @@ class BullQueue extends common_1.Queue {
33
36
  }
34
37
  }
35
38
  async checkHealth() {
39
+ // Reused, so an outage is logged once and not on every check
40
+ this._healthCheckQueue ??= new BullQueueProcessor('health-check');
36
41
  try {
37
- const queue = new BullQueueProcessor('health-check');
38
- await queue.close();
42
+ await this._healthCheckQueue.ensureConnected();
39
43
  return { success: true };
40
44
  }
41
45
  catch (e) {
@@ -47,9 +51,7 @@ exports.BullQueue = BullQueue;
47
51
  class BullQueueProcessor extends common_1.QueueProcessor {
48
52
  name;
49
53
  /** @internal */
50
- _connection = (0, context_1.inject)(common_1.Redis).createClient({
51
- maxRetriesPerRequest: null
52
- });
54
+ _connection;
53
55
  /** @internal */
54
56
  _queue;
55
57
  /** @internal */
@@ -59,6 +61,10 @@ class BullQueueProcessor extends common_1.QueueProcessor {
59
61
  constructor(name) {
60
62
  super();
61
63
  this.name = name;
64
+ this._connection = (0, context_1.inject)(common_1.Redis).createClient({
65
+ connectionName: `queue:${name}`,
66
+ maxRetriesPerRequest: null
67
+ });
62
68
  this._queue = new bullmq_1.Queue(name, {
63
69
  connection: this._connection, // Needed because of different ioredis types
64
70
  skipVersionCheck: true,
@@ -78,11 +84,18 @@ class BullQueueProcessor extends common_1.QueueProcessor {
78
84
  }
79
85
  }
80
86
  });
87
+ // Without a listener BullMQ prints every reconnection error, the outage
88
+ // itself is logged once by the Redis connection monitor
89
+ this._queue.on('error', (error) => {
90
+ common_1.logger.debug(error, `Queue '${name}' error`);
91
+ });
81
92
  }
82
93
  async sendJob(data, name, options = {}) {
94
+ await this.ensureConnected();
83
95
  return await this._queue.add(name ?? 'defaultJob', data, options);
84
96
  }
85
97
  async sendBulkJobs(jobs) {
98
+ await this.ensureConnected();
86
99
  return await this._queue.addBulk(jobs.map(({ data, name, options }) => ({
87
100
  data,
88
101
  name: name ?? 'defaultJob',
@@ -182,9 +195,43 @@ class BullQueueProcessor extends common_1.QueueProcessor {
182
195
  return true;
183
196
  }
184
197
  async close() {
198
+ const connected = this._connection.status === 'ready';
185
199
  await this._queue.close();
200
+ this.ignoreLateErrors(this._queue);
186
201
  for (const worker of this._workers) {
187
- await worker.close();
202
+ // Running jobs cannot finish while Redis is down
203
+ await worker.close(!connected);
204
+ this.ignoreLateErrors(worker);
205
+ }
206
+ // BullMQ does not close a connection it was given
207
+ if (connected) {
208
+ await this._connection.quit();
209
+ }
210
+ else {
211
+ this._connection.disconnect();
212
+ }
213
+ }
214
+ /**
215
+ * Fails fast while Redis is down, the queue would otherwise wait for the
216
+ * reconnection. Waits only for a connection that is still being established.
217
+ *
218
+ * @internal
219
+ */
220
+ async ensureConnected() {
221
+ const connection = this._connection;
222
+ if (['wait', 'connecting', 'connect'].includes(connection.status)) {
223
+ await new Promise((resolve) => {
224
+ const done = () => {
225
+ connection.off('ready', done);
226
+ connection.off('error', done);
227
+ resolve();
228
+ };
229
+ connection.once('ready', done);
230
+ connection.once('error', done);
231
+ });
232
+ }
233
+ if (connection.status !== 'ready') {
234
+ throw new Error(`Queue '${this.name}' is unavailable, Redis connection is not ready`);
188
235
  }
189
236
  }
190
237
  /** @internal */
@@ -204,4 +251,17 @@ class BullQueueProcessor extends common_1.QueueProcessor {
204
251
  }
205
252
  await Promise.allSettled(handlerActions);
206
253
  }
254
+ /**
255
+ * BullMQ removes the listeners of its connections on close, but one still
256
+ * connecting fails afterward and emits an unhandled error that crashes the
257
+ * process.
258
+ *
259
+ * @internal
260
+ */
261
+ ignoreLateErrors(target) {
262
+ const internal = target;
263
+ for (const key of ['connection', 'blockingConnection']) {
264
+ internal[key]?.on('error', () => undefined);
265
+ }
266
+ }
207
267
  }
@@ -1,6 +1,7 @@
1
1
  import { AuthScope, AuthSource, AuthUser, ResourceId, RouteConfig } from '@appweaver/common';
2
2
  import { AuthTokens, JwtPayload, UserAdditionalData } from '../types';
3
3
  export declare class AuthService {
4
+ constructor();
4
5
  /**
5
6
  * Finds an authenticated user by their unique identifier.
6
7
  *
@@ -19,6 +19,15 @@ class AuthService {
19
19
  /** Optional, since an application with no OAuth2 provider never registers it.
20
20
  * @internal */
21
21
  _oauth2Service = (0, context_1.inject)(oauth2_service_1.OAuth2Service, false);
22
+ /** @internal */
23
+ _events = (0, context_1.inject)(common_1.Events);
24
+ constructor() {
25
+ // Evicts the users changed outside of this service as well, e.g. through
26
+ // the resource routes, regardless of the cache invalidation strategy
27
+ const modelName = this._authUserService.modelName;
28
+ this._events.onResourceEvent(modelName, 'update', ({ previous, current }) => this.evictCachedUsers(previous, current));
29
+ this._events.onResourceEvent(modelName, 'delete', ({ current }) => this.evictCachedUsers(current));
30
+ }
22
31
  /**
23
32
  * Finds an authenticated user by their unique identifier.
24
33
  *
@@ -31,10 +40,7 @@ class AuthService {
31
40
  if (!common_1.config.CACHE_ENABLED || common_1.config.SECURITY_CACHE_TTL < 0) {
32
41
  return this._authUserService.find(id);
33
42
  }
34
- const cacheKey = this._cacheService.buildCacheKey({
35
- baseKey: `${AUTH_KEY}:${id}`,
36
- modelName: this._authUserService.modelName
37
- });
43
+ const cacheKey = this.authCacheKey(id);
38
44
  const value = await this._cacheService.getCachedValue(cacheKey);
39
45
  if (value) {
40
46
  return value;
@@ -59,10 +65,7 @@ class AuthService {
59
65
  const result = await this._authUserService.query({ email: username });
60
66
  return result.items[0] ?? null;
61
67
  }
62
- const cacheKey = this._cacheService.buildCacheKey({
63
- baseKey: `${AUTH_KEY}:${username}`,
64
- modelName: this._authUserService.modelName
65
- });
68
+ const cacheKey = this.authCacheKey(username);
66
69
  const value = await this._cacheService.getCachedValue(cacheKey);
67
70
  if (value) {
68
71
  return value;
@@ -87,12 +90,17 @@ class AuthService {
87
90
  * @return {Promise<AuthUser>} A promise that resolves to the updated authenticated user object.
88
91
  */
89
92
  async updateAuthUser(id, data) {
93
+ let authUser;
90
94
  try {
91
- return await this._authUserService.update(id, data);
95
+ authUser = await this._authUserService.update(id, data);
92
96
  }
93
97
  catch (e) {
94
98
  throw new errors_1.HttpError('Auth user update error', 500, e);
95
99
  }
100
+ // Awaited unlike the update event eviction, so the next request, e.g. with
101
+ // a token revoked by a logout, cannot read the previous state
102
+ await this.evictCachedUsers(authUser);
103
+ return authUser;
96
104
  }
97
105
  /**
98
106
  * Registers a new authenticated user based on the provided authentication source, email, and optional password and
@@ -217,9 +225,12 @@ class AuthService {
217
225
  authorize(authUser, url, routeConfig = {}, jwtPayload) {
218
226
  if (!authUser ||
219
227
  !authUser.enabled ||
228
+ // `iat` has a whole second precision, so a token issued within the same
229
+ // second as the logout is still accepted
220
230
  (authUser.logoutAt &&
221
231
  jwtPayload &&
222
- new Date(authUser.logoutAt).getTime() > jwtPayload?.iat)) {
232
+ Math.floor(new Date(authUser.logoutAt).getTime() / 1000) >
233
+ jwtPayload.iat)) {
223
234
  throw new errors_1.HttpError('Unauthorized access', 401);
224
235
  }
225
236
  if (jwtPayload && !(0, helper_1.checkScopeAccess)(url, jwtPayload.scope)) {
@@ -317,11 +328,11 @@ class AuthService {
317
328
  scope,
318
329
  source,
319
330
  username: authUser.email,
320
- sub: authUser.id,
321
- iat: new Date().getTime()
331
+ sub: authUser.id
322
332
  };
323
333
  const expiresIn = common_1.config.SECURITY_JWT_EXPIRES_IN;
324
334
  const refreshExpiresIn = common_1.config.SECURITY_JWT_REFRESH_EXPIRES_IN;
335
+ // The signer sets the `iat` claim, in seconds like the expiration
325
336
  const accessToken = server.jwt.sign(jwtPayload, { expiresIn });
326
337
  const refreshToken = server.jwt.sign({ ...jwtPayload, scope: common_1.AuthScope.Refresh }, { expiresIn: refreshExpiresIn });
327
338
  common_1.logger.debug({ id: authUser.id }, 'Access tokens generated');
@@ -335,9 +346,28 @@ class AuthService {
335
346
  * successful.
336
347
  */
337
348
  async logout(id) {
338
- await this._cacheService.removeCachedValue(`${AUTH_KEY}:${id}`);
339
349
  common_1.logger.debug({ id }, 'User logout');
340
350
  return !!(await this.updateAuthUser(id, { logoutAt: new Date() }));
341
351
  }
352
+ /** @internal */
353
+ async evictCachedUsers(...authUsers) {
354
+ const keys = new Set();
355
+ for (const authUser of authUsers) {
356
+ if (authUser) {
357
+ keys.add(this.authCacheKey(authUser.id));
358
+ keys.add(this.authCacheKey(authUser.email));
359
+ }
360
+ }
361
+ for (const key of keys) {
362
+ await this._cacheService.removeCachedValue(key);
363
+ }
364
+ }
365
+ /** @internal */
366
+ authCacheKey(idOrUsername) {
367
+ return this._cacheService.buildCacheKey({
368
+ baseKey: `${AUTH_KEY}:${idOrUsername}`,
369
+ modelName: this._authUserService.modelName
370
+ });
371
+ }
342
372
  }
343
373
  exports.AuthService = AuthService;
@@ -8,25 +8,29 @@ const shouldCreateModel = common_1.config.SECURITY_STORE_PROVIDER ===
8
8
  exports.default = shouldCreateModel
9
9
  ? (0, factory_1.createModel)({
10
10
  name: 'OneTimeToken',
11
+ // Tokens are only created and deleted, never updated
11
12
  audit: {
13
+ updatedAt: false,
12
14
  createdById: false
13
15
  },
14
16
  scalars: {
15
17
  tokenHash: {
16
18
  type: 'string',
17
- maxLength: 511
19
+ maxLength: 128,
20
+ unique: true
18
21
  },
19
22
  purpose: {
20
23
  type: 'string',
21
- maxLength: 255
22
- },
23
- expiresAt: {
24
- type: 'dateTime'
24
+ maxLength: 64
25
25
  },
26
26
  data: {
27
27
  type: 'json'
28
+ },
29
+ expiresAt: {
30
+ type: 'dateTime'
28
31
  }
29
32
  },
30
- index: [['tokenHash', 'purpose']]
33
+ // Deleting the expired tokens filters by the expiration
34
+ index: [['expiresAt']]
31
35
  })
32
36
  : undefined;
@@ -9,44 +9,47 @@ class DatabaseSecurityStore extends common_1.SecurityStore {
9
9
  _db = (0, context_1.inject)(common_1.Database);
10
10
  async generateOneTimeToken(purpose, data, ttl) {
11
11
  const token = (0, common_1.generateToken)('bytes', 64);
12
- await this._db.client().oneTimeToken.create({
12
+ const oneTimeTokens = this._db.client().oneTimeToken;
13
+ // Expired tokens are removed here, so they need no scheduled cleanup
14
+ await oneTimeTokens.deleteMany({
15
+ where: { expiresAt: { lt: new Date() } }
16
+ });
17
+ await oneTimeTokens.create({
13
18
  data: {
14
19
  tokenHash: (0, common_1.makeHash)(token),
15
20
  purpose,
16
- expiresAt: new Date(Date.now() + ttl),
17
- data: data
21
+ data: data,
22
+ expiresAt: new Date(Date.now() + ttl)
18
23
  }
19
24
  });
20
25
  return token;
21
26
  }
22
27
  async useOneTimeToken(token, purpose, validateContent) {
23
- const oneTimeToken = await this._db.client().oneTimeToken.findFirst({
24
- where: {
25
- purpose,
26
- tokenHash: (0, common_1.makeHash)(token)
27
- }
28
+ const oneTimeTokens = this._db.client().oneTimeToken;
29
+ const oneTimeToken = await oneTimeTokens.findUnique({
30
+ where: { tokenHash: (0, common_1.makeHash)(token) }
28
31
  });
29
32
  if (oneTimeToken === null ||
33
+ oneTimeToken.purpose !== purpose ||
30
34
  oneTimeToken.expiresAt.getTime() < Date.now()) {
31
- if (oneTimeToken) {
32
- await this.removeOneTimeToken(oneTimeToken.id);
33
- }
34
35
  throw new errors_1.HttpError('Invalid or expired token provided', 401);
35
36
  }
36
37
  const data = oneTimeToken.data;
38
+ // A token failing the validation is kept, so it can be used again
37
39
  if (validateContent) {
38
40
  const result = validateContent(data);
39
41
  if (!result.valid) {
40
42
  throw new errors_1.HttpError(result.message, 401);
41
43
  }
42
44
  }
43
- await this.removeOneTimeToken(oneTimeToken.id);
45
+ // Only the one of concurrent uses that removes the token gets its data
46
+ const { count } = await oneTimeTokens.deleteMany({
47
+ where: { id: oneTimeToken.id }
48
+ });
49
+ if (count === 0) {
50
+ throw new errors_1.HttpError('Invalid or expired token provided', 401);
51
+ }
44
52
  return data;
45
53
  }
46
- /** @internal */
47
- async removeOneTimeToken(id) {
48
- // The generated client types the id after the configured primary key
49
- await this._db.client().oneTimeToken.delete({ where: { id: id } });
50
- }
51
54
  }
52
55
  exports.DatabaseSecurityStore = DatabaseSecurityStore;
@@ -19,13 +19,18 @@ class RedisSecurityStore extends common_1.SecurityStore {
19
19
  if (value === null) {
20
20
  throw new errors_1.HttpError('Invalid or expired token provided', 401);
21
21
  }
22
+ // A token failing the validation is kept, so it can be used again
22
23
  if (validateContent) {
23
24
  const result = validateContent(value);
24
25
  if (!result.valid) {
25
26
  throw new errors_1.HttpError(result.message, 401);
26
27
  }
27
28
  }
28
- await this._redis.removeValue(tokenKey);
29
+ // Only the one of concurrent uses that removes the token gets its data
30
+ const removed = await this._redis.removeValue(tokenKey);
31
+ if (!removed) {
32
+ throw new errors_1.HttpError('Invalid or expired token provided', 401);
33
+ }
29
34
  return value;
30
35
  }
31
36
  }