@flowerforce/flowerbase 1.7.6-beta.1 → 1.7.6-beta.10

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.
Files changed (70) hide show
  1. package/dist/auth/providers/anon-user/controller.d.ts.map +1 -1
  2. package/dist/auth/providers/anon-user/controller.js +1 -0
  3. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  4. package/dist/auth/providers/custom-function/controller.js +3 -1
  5. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  6. package/dist/auth/providers/local-userpass/controller.js +58 -18
  7. package/dist/auth/providers/local-userpass/dtos.d.ts +5 -1
  8. package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
  9. package/dist/auth/utils.d.ts +1 -0
  10. package/dist/auth/utils.d.ts.map +1 -1
  11. package/dist/auth/utils.js +1 -0
  12. package/dist/features/endpoints/utils.d.ts.map +1 -1
  13. package/dist/features/endpoints/utils.js +3 -0
  14. package/dist/features/functions/controller.d.ts +2 -0
  15. package/dist/features/functions/controller.d.ts.map +1 -1
  16. package/dist/features/functions/controller.js +7 -1
  17. package/dist/features/rules/interface.d.ts +6 -5
  18. package/dist/features/rules/interface.d.ts.map +1 -1
  19. package/dist/features/triggers/interface.d.ts +1 -1
  20. package/dist/features/triggers/interface.d.ts.map +1 -1
  21. package/dist/features/triggers/utils.d.ts.map +1 -1
  22. package/dist/features/triggers/utils.js +60 -0
  23. package/dist/services/mongodb-atlas/index.d.ts +3 -0
  24. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  25. package/dist/services/mongodb-atlas/index.js +128 -37
  26. package/dist/utils/context/helpers.d.ts.map +1 -1
  27. package/dist/utils/context/helpers.js +3 -2
  28. package/dist/utils/context/index.d.ts.map +1 -1
  29. package/dist/utils/context/index.js +4 -2
  30. package/dist/utils/roles/helpers.d.ts.map +1 -1
  31. package/dist/utils/roles/helpers.js +6 -3
  32. package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
  33. package/dist/utils/roles/machines/fieldPermissions.js +19 -10
  34. package/dist/utils/rules-matcher/interface.d.ts +2 -0
  35. package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
  36. package/dist/utils/rules-matcher/interface.js +1 -0
  37. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  38. package/dist/utils/rules-matcher/utils.js +23 -6
  39. package/package.json +1 -1
  40. package/src/auth/providers/anon-user/controller.ts +1 -0
  41. package/src/auth/providers/custom-function/controller.ts +6 -1
  42. package/src/auth/providers/local-userpass/__tests__/controller.test.ts +200 -0
  43. package/src/auth/providers/local-userpass/controller.ts +87 -34
  44. package/src/auth/providers/local-userpass/dtos.ts +6 -1
  45. package/src/auth/utils.ts +1 -0
  46. package/src/features/endpoints/__tests__/utils.test.ts +65 -0
  47. package/src/features/endpoints/utils.ts +3 -0
  48. package/src/features/functions/__tests__/watch-filter.test.ts +11 -1
  49. package/src/features/functions/controller.ts +8 -0
  50. package/src/features/rules/interface.ts +18 -17
  51. package/src/features/triggers/__tests__/index.test.ts +6 -4
  52. package/src/features/triggers/interface.ts +1 -1
  53. package/src/features/triggers/utils.ts +60 -0
  54. package/src/monitoring/ui.events.js +1 -1
  55. package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +205 -7
  56. package/src/services/mongodb-atlas/__tests__/utils.test.ts +27 -0
  57. package/src/services/mongodb-atlas/__tests__/watch-filter.test.ts +78 -0
  58. package/src/services/mongodb-atlas/index.ts +371 -171
  59. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +74 -5
  60. package/src/utils/__tests__/contextExecuteCompatibility.test.ts +27 -1
  61. package/src/utils/__tests__/evaluateExpression.test.ts +33 -0
  62. package/src/utils/__tests__/generateContextData.test.ts +5 -1
  63. package/src/utils/__tests__/rule.test.ts +38 -0
  64. package/src/utils/context/helpers.ts +3 -2
  65. package/src/utils/context/index.ts +4 -3
  66. package/src/utils/roles/helpers.ts +10 -5
  67. package/src/utils/roles/machines/fieldPermissions.ts +17 -8
  68. package/src/utils/rules-matcher/interface.ts +2 -0
  69. package/src/utils/rules-matcher/utils.ts +33 -17
  70. package/src/utils/__tests__/readFileContent.test.ts +0 -35
@@ -128,15 +128,20 @@ export async function customFunctionController(app: FastifyInstance) {
128
128
  ...(user || {})
129
129
  }
130
130
  }
131
+ const now = new Date()
131
132
  const refreshToken = this.createRefreshToken(currentUserData)
132
133
  const refreshTokenHash = hashToken(refreshToken)
133
134
  await authDb.collection(refreshTokensCollection).insertOne({
134
135
  userId: authUser._id,
135
136
  tokenHash: refreshTokenHash,
136
- createdAt: new Date(),
137
+ createdAt: now,
137
138
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
138
139
  revokedAt: null
139
140
  })
141
+ await authDb.collection(authCollection!).updateOne(
142
+ { _id: authUser._id },
143
+ { $set: { lastLoginAt: now } }
144
+ )
140
145
  return {
141
146
  access_token: this.createAccessToken(currentUserData),
142
147
  refresh_token: refreshToken,
@@ -0,0 +1,200 @@
1
+ jest.mock('../../../../constants', () => ({
2
+ AUTH_CONFIG: {
3
+ authCollection: 'auth_users',
4
+ refreshTokensCollection: 'refresh_tokens',
5
+ resetPasswordCollection: 'reset_password_requests',
6
+ userCollection: 'users',
7
+ user_id_field: 'id',
8
+ authProviders: {
9
+ 'local-userpass': {
10
+ disabled: false
11
+ }
12
+ },
13
+ resetPasswordConfig: {
14
+ runResetFunction: true,
15
+ resetFunctionName: 'customReset'
16
+ }
17
+ },
18
+ AUTH_DB_NAME: 'test-auth-db',
19
+ DB_NAME: 'test-db',
20
+ DEFAULT_CONFIG: {
21
+ RESET_PASSWORD_TTL_SECONDS: 3600,
22
+ AUTH_RATE_LIMIT_WINDOW_MS: 60000,
23
+ AUTH_LOGIN_MAX_ATTEMPTS: 5,
24
+ AUTH_REGISTER_MAX_ATTEMPTS: 5,
25
+ AUTH_RESET_MAX_ATTEMPTS: 5,
26
+ REFRESH_TOKEN_TTL_DAYS: 1
27
+ }
28
+ }))
29
+
30
+ jest.mock('../../../../state', () => ({
31
+ StateManager: {
32
+ select: jest.fn((key: string) => {
33
+ if (key === 'functions') {
34
+ return {
35
+ customReset: { name: 'customReset', code: 'exports = async () => ({ status: "success" })' }
36
+ }
37
+ }
38
+ if (key === 'services') {
39
+ return {}
40
+ }
41
+ return {}
42
+ })
43
+ }
44
+ }))
45
+
46
+ jest.mock('../../../../utils/context', () => ({
47
+ GenerateContext: jest.fn()
48
+ }))
49
+
50
+ jest.mock('../../../../utils/crypto', () => ({
51
+ comparePassword: jest.fn(),
52
+ generateToken: jest.fn(() => 'generated-token'),
53
+ hashPassword: jest.fn(async (password: string) => `hashed:${password}`),
54
+ hashToken: jest.fn(() => 'hashed-token')
55
+ }))
56
+
57
+ import { AUTH_ERRORS } from '../../../utils'
58
+ import { localUserPassController } from '../controller'
59
+ import { GenerateContext } from '../../../../utils/context'
60
+ import { hashPassword } from '../../../../utils/crypto'
61
+
62
+ describe('localUserPassController reset call', () => {
63
+ const buildApp = () => {
64
+ let resetCallHandler:
65
+ | ((req: { body: { email: string; password: string; arguments?: unknown[] }; ip: string }, res: { status: jest.Mock }) => Promise<unknown>)
66
+ | undefined
67
+
68
+ const authUsersCollection = {
69
+ findOne: jest.fn().mockResolvedValue({
70
+ _id: 'auth-user-1',
71
+ email: 'john@doe.com',
72
+ password: 'old-hash'
73
+ }),
74
+ updateOne: jest.fn().mockResolvedValue({ acknowledged: true })
75
+ }
76
+ const resetCollection = {
77
+ createIndex: jest.fn().mockResolvedValue('ok'),
78
+ updateOne: jest.fn().mockResolvedValue({ acknowledged: true }),
79
+ deleteOne: jest.fn().mockResolvedValue({ acknowledged: true }),
80
+ findOne: jest.fn()
81
+ }
82
+ const refreshCollection = {
83
+ createIndex: jest.fn().mockResolvedValue('ok'),
84
+ insertOne: jest.fn()
85
+ }
86
+ const usersCollection = {
87
+ findOne: jest.fn()
88
+ }
89
+ const db = {
90
+ collection: jest.fn((name: string) => {
91
+ if (name === 'auth_users') return authUsersCollection
92
+ if (name === 'reset_password_requests') return resetCollection
93
+ if (name === 'refresh_tokens') return refreshCollection
94
+ if (name === 'users') return usersCollection
95
+ return {}
96
+ })
97
+ }
98
+ const app = {
99
+ mongo: { client: { db: jest.fn().mockReturnValue(db) } },
100
+ post: jest.fn((path: string, _opts: unknown, handler: typeof resetCallHandler) => {
101
+ if (path === '/reset/call') {
102
+ resetCallHandler = handler
103
+ }
104
+ })
105
+ }
106
+
107
+ return { app, authUsersCollection, resetCollection, resetCallHandlerRef: () => resetCallHandler }
108
+ }
109
+
110
+ beforeEach(() => {
111
+ jest.clearAllMocks()
112
+ })
113
+
114
+ it('hashes and applies the password when the custom reset function returns success', async () => {
115
+ ;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'success' })
116
+ const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
117
+
118
+ await localUserPassController(app as never)
119
+
120
+ const res = { status: jest.fn() }
121
+ const result = await resetCallHandlerRef()?.(
122
+ {
123
+ body: { email: 'john@doe.com', password: 'new-secret', arguments: ['extra'] },
124
+ ip: '127.0.0.1'
125
+ },
126
+ res
127
+ )
128
+
129
+ expect(GenerateContext).toHaveBeenCalledWith(expect.objectContaining({
130
+ args: [
131
+ {
132
+ token: 'generated-token',
133
+ tokenId: 'generated-token',
134
+ email: 'john@doe.com',
135
+ password: 'new-secret',
136
+ username: 'john@doe.com'
137
+ },
138
+ 'extra'
139
+ ],
140
+ runAsSystem: true
141
+ }))
142
+ expect(hashPassword).toHaveBeenCalledWith('new-secret')
143
+ expect(authUsersCollection.updateOne).toHaveBeenCalledWith(
144
+ { email: 'john@doe.com' },
145
+ { $set: { password: 'hashed:new-secret' } }
146
+ )
147
+ expect(resetCollection.deleteOne).toHaveBeenCalledWith({ email: 'john@doe.com' })
148
+ expect(res.status).toHaveBeenCalledWith(202)
149
+ expect(result).toEqual({ status: 'success' })
150
+ })
151
+
152
+ it('returns pending without changing the password when the custom reset function returns pending', async () => {
153
+ ;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'pending' })
154
+ const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
155
+
156
+ await localUserPassController(app as never)
157
+
158
+ const res = { status: jest.fn() }
159
+ const result = await resetCallHandlerRef()?.(
160
+ {
161
+ body: { email: 'john@doe.com', password: 'new-secret' },
162
+ ip: '127.0.0.1'
163
+ },
164
+ res
165
+ )
166
+
167
+ expect(hashPassword).not.toHaveBeenCalled()
168
+ expect(authUsersCollection.updateOne).not.toHaveBeenCalledWith(
169
+ { email: 'john@doe.com' },
170
+ expect.objectContaining({ $set: { password: expect.any(String) } })
171
+ )
172
+ expect(resetCollection.deleteOne).not.toHaveBeenCalled()
173
+ expect(res.status).toHaveBeenCalledWith(202)
174
+ expect(result).toEqual({ status: 'pending' })
175
+ })
176
+
177
+ it('rejects the request when the custom reset function returns fail', async () => {
178
+ ;(GenerateContext as jest.Mock).mockResolvedValue({ status: 'fail' })
179
+ const { app, authUsersCollection, resetCollection, resetCallHandlerRef } = buildApp()
180
+
181
+ await localUserPassController(app as never)
182
+
183
+ const res = { status: jest.fn() }
184
+
185
+ await expect(
186
+ resetCallHandlerRef()?.(
187
+ {
188
+ body: { email: 'john@doe.com', password: 'new-secret' },
189
+ ip: '127.0.0.1'
190
+ },
191
+ res
192
+ )
193
+ ).rejects.toThrow(AUTH_ERRORS.INVALID_RESET_PARAMS)
194
+
195
+ expect(hashPassword).not.toHaveBeenCalled()
196
+ expect(authUsersCollection.updateOne).not.toHaveBeenCalled()
197
+ expect(resetCollection.deleteOne).not.toHaveBeenCalled()
198
+ expect(res.status).not.toHaveBeenCalled()
199
+ })
200
+ })
@@ -5,7 +5,12 @@ import handleUserRegistration from '../../../shared/handleUserRegistration'
5
5
  import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
6
6
  import { StateManager } from '../../../state'
7
7
  import { GenerateContext } from '../../../utils/context'
8
- import { comparePassword, generateToken, hashPassword, hashToken } from '../../../utils/crypto'
8
+ import {
9
+ comparePassword,
10
+ generateToken,
11
+ hashPassword,
12
+ hashToken
13
+ } from '../../../utils/crypto'
9
14
  import {
10
15
  AUTH_ENDPOINTS,
11
16
  AUTH_ERRORS,
@@ -27,6 +32,8 @@ import {
27
32
 
28
33
  const rateLimitStore = new Map<string, number[]>()
29
34
 
35
+ type ResetFunctionResult = { status?: 'success' | 'pending' | 'fail' }
36
+
30
37
  const isRateLimited = (key: string, maxAttempts: number, windowMs: number) => {
31
38
  const now = Date.now()
32
39
  const existing = rateLimitStore.get(key) ?? []
@@ -53,21 +60,23 @@ export async function localUserPassController(app: FastifyInstance) {
53
60
  const resetMaxAttempts = DEFAULT_CONFIG.AUTH_RESET_MAX_ATTEMPTS
54
61
  const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
55
62
  const resolveLocalUserpassProvider = () => AUTH_CONFIG.authProviders?.['local-userpass']
63
+ const invalidPasswordError = {
64
+ error: 'unauthorized',
65
+ error_code: 'InvalidPassword'
66
+ } as const
56
67
 
57
68
  try {
58
- await authDb.collection(resetPasswordCollection).createIndex(
59
- { createdAt: 1 },
60
- { expireAfterSeconds: resetPasswordTtlSeconds }
61
- )
69
+ await authDb
70
+ .collection(resetPasswordCollection)
71
+ .createIndex({ createdAt: 1 }, { expireAfterSeconds: resetPasswordTtlSeconds })
62
72
  } catch (error) {
63
73
  console.error('Failed to ensure reset password TTL index', error)
64
74
  }
65
75
 
66
76
  try {
67
- await authDb.collection(refreshTokensCollection).createIndex(
68
- { expiresAt: 1 },
69
- { expireAfterSeconds: 0 }
70
- )
77
+ await authDb
78
+ .collection(refreshTokensCollection)
79
+ .createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
71
80
  } catch (error) {
72
81
  console.error('Failed to ensure refresh token TTL index', error)
73
82
  }
@@ -105,8 +114,10 @@ export async function localUserPassController(app: FastifyInstance) {
105
114
  const services = StateManager.select('services')
106
115
  const currentFunction = functionsList[resetPasswordConfig.resetFunctionName]
107
116
  const baseArgs = { token, tokenId, email, password, username: email }
108
- const args = Array.isArray(extraArguments) ? [baseArgs, ...extraArguments] : [baseArgs]
109
- await GenerateContext({
117
+ const args = Array.isArray(extraArguments)
118
+ ? [baseArgs, ...extraArguments]
119
+ : [baseArgs]
120
+ const response = (await GenerateContext({
110
121
  args,
111
122
  app,
112
123
  rules: {},
@@ -114,11 +125,41 @@ export async function localUserPassController(app: FastifyInstance) {
114
125
  currentFunction,
115
126
  functionName: resetPasswordConfig.resetFunctionName,
116
127
  functionsList,
117
- services
118
- })
119
- return
128
+ services,
129
+ runAsSystem: true
130
+ })) as ResetFunctionResult
131
+ const resetStatus = response?.status
132
+
133
+ if (resetStatus === 'success') {
134
+ if (!password) {
135
+ throw new Error(AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE)
136
+ }
137
+
138
+ const hashedPassword = await hashPassword(password)
139
+ await authDb.collection(authCollection!).updateOne(
140
+ { email },
141
+ {
142
+ $set: {
143
+ password: hashedPassword
144
+ }
145
+ }
146
+ )
147
+ await authDb?.collection(resetPasswordCollection).deleteOne({ email })
148
+ return { status: 'success' as const }
149
+ }
150
+
151
+ if (resetStatus === 'pending') {
152
+ return { status: 'pending' as const }
153
+ }
154
+
155
+ if (resetStatus === 'fail') {
156
+ throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
157
+ }
158
+
159
+ throw new Error(AUTH_ERRORS.INVALID_RESET_FUNCTION_RESPONSE)
120
160
  }
121
161
 
162
+ return { status: 'pending' as const }
122
163
  }
123
164
 
124
165
  /**
@@ -147,12 +188,18 @@ export async function localUserPassController(app: FastifyInstance) {
147
188
 
148
189
  let result
149
190
  try {
150
- result = await handleUserRegistration(app, { run_as_system: true, provider: PROVIDER.LOCAL_USERPASS })({
191
+ result = await handleUserRegistration(app, {
192
+ run_as_system: true,
193
+ provider: PROVIDER.LOCAL_USERPASS
194
+ })({
151
195
  email: req.body.email.toLowerCase(),
152
196
  password: req.body.password
153
197
  })
154
198
  } catch (error) {
155
- if (error instanceof Error && error.message === 'This email address is already used') {
199
+ if (
200
+ error instanceof Error &&
201
+ error.message === 'This email address is already used'
202
+ ) {
156
203
  res.status(409).send({
157
204
  error: 'name already in use',
158
205
  error_code: 'AccountNameInUse'
@@ -195,10 +242,10 @@ export async function localUserPassController(app: FastifyInstance) {
195
242
  return
196
243
  }
197
244
 
198
- const existing = await authDb.collection(authCollection!).findOne({
245
+ const existing = (await authDb.collection(authCollection!).findOne({
199
246
  confirmationToken: req.body.token,
200
247
  confirmationTokenId: req.body.tokenId
201
- }) as { _id: ObjectId; status?: string } | null
248
+ })) as { _id: ObjectId; status?: string } | null
202
249
 
203
250
  if (!existing) {
204
251
  res.status(500)
@@ -247,23 +294,22 @@ export async function localUserPassController(app: FastifyInstance) {
247
294
  })
248
295
 
249
296
  if (!authUser) {
250
- throw new Error(AUTH_ERRORS.INVALID_CREDENTIALS)
297
+ res.status(401)
298
+ return invalidPasswordError
251
299
  }
252
300
 
253
- const passwordMatches = await comparePassword(
254
- req.body.password,
255
- authUser.password
256
- )
301
+ const passwordMatches = await comparePassword(req.body.password, authUser.password)
257
302
 
258
303
  if (!passwordMatches) {
259
- throw new Error(AUTH_ERRORS.INVALID_CREDENTIALS)
304
+ res.status(401)
305
+ return invalidPasswordError
260
306
  }
261
307
 
262
308
  const user =
263
309
  user_id_field && userCollection
264
310
  ? await customUserDb
265
- .collection(userCollection)
266
- .findOne({ [user_id_field]: authUser._id.toString() })
311
+ .collection(userCollection)
312
+ .findOne({ [user_id_field]: authUser._id.toString() })
267
313
  : {}
268
314
  delete authUser?.password
269
315
 
@@ -283,15 +329,19 @@ export async function localUserPassController(app: FastifyInstance) {
283
329
  throw new Error(AUTH_ERRORS.USER_NOT_CONFIRMED)
284
330
  }
285
331
 
332
+ const now = new Date()
286
333
  const refreshToken = this.createRefreshToken(userWithCustomData)
287
334
  const refreshTokenHash = hashToken(refreshToken)
288
335
  await authDb.collection(refreshTokensCollection).insertOne({
289
336
  userId: authUser._id,
290
337
  tokenHash: refreshTokenHash,
291
- createdAt: new Date(),
338
+ createdAt: now,
292
339
  expiresAt: new Date(Date.now() + refreshTokenTtlMs),
293
340
  revokedAt: null
294
341
  })
342
+ await authDb
343
+ .collection(authCollection!)
344
+ .updateOne({ _id: authUser._id }, { $set: { lastLoginAt: now } })
295
345
 
296
346
  return {
297
347
  access_token: this.createAccessToken(userWithCustomData),
@@ -347,15 +397,13 @@ export async function localUserPassController(app: FastifyInstance) {
347
397
  res.status(429)
348
398
  return { message: 'Too many requests' }
349
399
  }
350
- await handleResetPasswordRequest(
400
+ const result = await handleResetPasswordRequest(
351
401
  req.body.email,
352
402
  req.body.password,
353
403
  req.body.arguments
354
404
  )
355
405
  res.status(202)
356
- return {
357
- status: 'ok'
358
- }
406
+ return result
359
407
  }
360
408
  )
361
409
 
@@ -392,12 +440,15 @@ export async function localUserPassController(app: FastifyInstance) {
392
440
  }
393
441
 
394
442
  const createdAt = resetRequest.createdAt ? new Date(resetRequest.createdAt) : null
395
- const isExpired = !createdAt ||
443
+ const isExpired =
444
+ !createdAt ||
396
445
  Number.isNaN(createdAt.getTime()) ||
397
446
  Date.now() - createdAt.getTime() > resetPasswordTtlSeconds * 1000
398
447
 
399
448
  if (isExpired) {
400
- await authDb?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
449
+ await authDb
450
+ ?.collection(resetPasswordCollection)
451
+ .deleteOne({ _id: resetRequest._id })
401
452
  throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
402
453
  }
403
454
  const hashedPassword = await hashPassword(password)
@@ -410,7 +461,9 @@ export async function localUserPassController(app: FastifyInstance) {
410
461
  }
411
462
  )
412
463
 
413
- await authDb?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
464
+ await authDb
465
+ ?.collection(resetPasswordCollection)
466
+ .deleteOne({ _id: resetRequest._id })
414
467
  }
415
468
  )
416
469
  }
@@ -19,13 +19,18 @@ export type ErrorResponseDto = {
19
19
  message: string
20
20
  }
21
21
 
22
+ export type InvalidPasswordResponseDto = {
23
+ error: 'unauthorized'
24
+ error_code: 'InvalidPassword'
25
+ }
26
+
22
27
  export interface RegistrationDto {
23
28
  Body: RegisterUserDto
24
29
  }
25
30
 
26
31
  export interface LoginDto {
27
32
  Body: LoginUserDto
28
- Reply: LoginSuccessDto | ErrorResponseDto
33
+ Reply: LoginSuccessDto | ErrorResponseDto | InvalidPasswordResponseDto
29
34
  }
30
35
 
31
36
  export interface ResetPasswordSendDto {
package/src/auth/utils.ts CHANGED
@@ -117,6 +117,7 @@ export enum AUTH_ERRORS {
117
117
  INVALID_TOKEN = 'Invalid refresh token provided',
118
118
  INVALID_RESET_PARAMS = 'Invalid token or tokenId provided',
119
119
  MISSING_RESET_FUNCTION = 'Missing reset function',
120
+ INVALID_RESET_FUNCTION_RESPONSE = 'Invalid reset function response',
120
121
  USER_NOT_CONFIRMED = 'User not confirmed'
121
122
  }
122
123
 
@@ -0,0 +1,65 @@
1
+ import { GenerateContext } from '../../../utils/context'
2
+ import { generateHandler } from '../utils'
3
+
4
+ jest.mock('../../../utils/context', () => ({
5
+ GenerateContext: jest.fn()
6
+ }))
7
+
8
+ const mockedGenerateContext = jest.mocked(GenerateContext)
9
+
10
+ describe('generateHandler', () => {
11
+ beforeEach(() => {
12
+ mockedGenerateContext.mockReset()
13
+ })
14
+
15
+ it('allows endpoint functions to set custom response headers', async () => {
16
+ mockedGenerateContext.mockImplementation(async ({ args }) => {
17
+ const [, response] = args as [
18
+ { body: { text: () => string; rawBody: Buffer | string | undefined } },
19
+ {
20
+ setStatusCode: (code: number) => void
21
+ setHeader: (name: string, value: string | number | readonly string[]) => void
22
+ setBody: (body: unknown) => void
23
+ }
24
+ ]
25
+
26
+ response.setStatusCode(201)
27
+ response.setHeader('Content-Type', 'application/json')
28
+ response.setHeader('Cache-Control', 'no-store')
29
+ response.setBody(JSON.stringify({ ok: true }))
30
+
31
+ return { ignored: true }
32
+ })
33
+
34
+ const handler = generateHandler({
35
+ app: {} as any,
36
+ currentFunction: { code: 'module.exports = function () {}' } as any,
37
+ functionName: 'endpointHandler',
38
+ functionsList: {
39
+ endpointHandler: { code: 'module.exports = function () {}' }
40
+ } as any,
41
+ http_method: 'POST',
42
+ rulesList: {} as any
43
+ })
44
+
45
+ const res = {
46
+ status: jest.fn(),
47
+ header: jest.fn(),
48
+ send: jest.fn((body) => body)
49
+ } as any
50
+
51
+ const response = await handler({
52
+ body: { hello: 'world' },
53
+ headers: { accept: 'application/json' },
54
+ query: { page: '1' },
55
+ rawBody: '{"hello":"world"}',
56
+ user: { id: 'user-1' }
57
+ } as any, res)
58
+
59
+ expect(res.status).toHaveBeenCalledWith(201)
60
+ expect(res.header).toHaveBeenCalledWith('Content-Type', 'application/json')
61
+ expect(res.header).toHaveBeenCalledWith('Cache-Control', 'no-store')
62
+ expect(res.send).toHaveBeenCalledWith(JSON.stringify({ ok: true }))
63
+ expect(response).toBe(JSON.stringify({ ok: true }))
64
+ })
65
+ })
@@ -138,6 +138,9 @@ export const generateHandler = ({
138
138
  setStatusCode: (code: number) => {
139
139
  res.status(code)
140
140
  },
141
+ setHeader: (name: string, value: string | number | readonly string[]) => {
142
+ res.header(name, value)
143
+ },
141
144
  setBody: (body: unknown) => {
142
145
  customResponseBody.data = body
143
146
  }
@@ -1,5 +1,9 @@
1
1
  import { ObjectId } from 'mongodb'
2
- import { mapWatchFilterToChangeStreamMatch, mapWatchFilterToDocumentQuery } from '../controller'
2
+ import {
3
+ mapWatchFilterToChangeStreamMatch,
4
+ mapWatchFilterToDocumentQuery,
5
+ shouldSkipReadabilityLookupForChange
6
+ } from '../controller'
3
7
 
4
8
  describe('watch filter mapping', () => {
5
9
  it('keeps change-event fields untouched and prefixes only document fields', () => {
@@ -113,4 +117,10 @@ describe('watch filter mapping', () => {
113
117
  expect(documentQuery._id).toEqual(id)
114
118
  expect(documentQuery.operationType).toBeUndefined()
115
119
  })
120
+
121
+ it('skips readability lookup only for delete change events', () => {
122
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'delete' } as any)).toBe(true)
123
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'update' } as any)).toBe(false)
124
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'insert' } as any)).toBe(false)
125
+ })
116
126
  })
@@ -315,6 +315,9 @@ const isReadableDocumentResult = (value: unknown) =>
315
315
  !Array.isArray(value) &&
316
316
  Object.keys(value as Record<string, unknown>).length > 0
317
317
 
318
+ export const shouldSkipReadabilityLookupForChange = (change: Document) =>
319
+ change.operationType === 'delete'
320
+
318
321
  /**
319
322
  * > Creates a pre handler for every query
320
323
  * @param app -> the fastify instance
@@ -524,6 +527,11 @@ export const functionsController: FunctionController = async (
524
527
  (change as { fullDocument?: { _id?: unknown } })?.fullDocument?._id
525
528
  if (typeof docId === 'undefined') return
526
529
 
530
+ if (shouldSkipReadabilityLookupForChange(change)) {
531
+ subscriberRes.write(`data: ${serializeEjson(change)}\n\n`)
532
+ return
533
+ }
534
+
527
535
  const readQuery = subscriber.documentFilter
528
536
  ? ({ $and: [subscriber.documentFilter, { _id: docId }] } as Document)
529
537
  : ({ _id: docId } as Document)
@@ -1,4 +1,6 @@
1
1
  import { Document } from 'mongodb'
2
+ export type PermissionExpression = boolean | Record<string, unknown>
3
+
2
4
  export interface Filter {
3
5
  name: string
4
6
  query: Record<string, unknown>
@@ -9,11 +11,11 @@ export type Projection = Record<string, 0 | 1>
9
11
  export interface Role {
10
12
  name: string
11
13
  apply_when: Record<string, unknown>
12
- insert: boolean
13
- delete: boolean
14
- search: boolean
15
- read: boolean
16
- write: boolean
14
+ insert: PermissionExpression
15
+ delete: PermissionExpression
16
+ search: PermissionExpression
17
+ read: PermissionExpression
18
+ write: PermissionExpression
17
19
  }
18
20
 
19
21
  export interface RulesConfig {
@@ -21,7 +23,6 @@ export interface RulesConfig {
21
23
  collection: string
22
24
  filters: Filter[]
23
25
  roles: Role[]
24
-
25
26
  }
26
27
 
27
28
  export type Rules = Record<string, RulesConfig>
@@ -38,21 +39,21 @@ export type AggregationPipelineStage =
38
39
  | { $unionWith: UnionWithStage }
39
40
 
40
41
  export interface LookupStage {
41
- from: string;
42
- localField?: string;
43
- foreignField?: string;
44
- as: string;
45
- let?: Record<string, unknown>;
46
- pipeline?: AggregationPipelineStage[];
42
+ from: string
43
+ localField?: string
44
+ foreignField?: string
45
+ as: string
46
+ let?: Record<string, unknown>
47
+ pipeline?: AggregationPipelineStage[]
47
48
  }
48
49
 
49
50
  export type AggregationPipeline = Document[]
50
51
 
51
52
  export type UnionWithStage = string | UnionWithNestedStage
52
- type UnionWithNestedStage = { coll: string, pipeline: AggregationPipelineStage[] }
53
+ type UnionWithNestedStage = { coll: string; pipeline: AggregationPipelineStage[] }
53
54
 
54
55
  export enum STAGES_TO_SEARCH {
55
- LOOKUP = "$lookup",
56
- UNION_WITH = "$unionWith",
57
- FACET = "$facet"
58
- }
56
+ LOOKUP = '$lookup',
57
+ UNION_WITH = '$unionWith',
58
+ FACET = '$facet'
59
+ }