@flowerforce/flowerbase 1.7.5 → 1.7.6-beta.1

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 (108) hide show
  1. package/README.md +125 -1
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +11 -10
  4. package/dist/auth/plugins/jwt.js +1 -1
  5. package/dist/auth/providers/anon-user/controller.js +1 -1
  6. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/custom-function/controller.js +28 -7
  8. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  9. package/dist/auth/providers/local-userpass/controller.js +15 -14
  10. package/dist/auth/utils.d.ts +1 -0
  11. package/dist/auth/utils.d.ts.map +1 -1
  12. package/dist/constants.d.ts +11 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +14 -3
  15. package/dist/features/encryption/interface.d.ts +36 -0
  16. package/dist/features/encryption/interface.d.ts.map +1 -0
  17. package/dist/features/encryption/interface.js +2 -0
  18. package/dist/features/encryption/utils.d.ts +9 -0
  19. package/dist/features/encryption/utils.d.ts.map +1 -0
  20. package/dist/features/encryption/utils.js +34 -0
  21. package/dist/features/rules/utils.d.ts.map +1 -1
  22. package/dist/features/rules/utils.js +1 -11
  23. package/dist/features/triggers/index.d.ts.map +1 -1
  24. package/dist/features/triggers/index.js +5 -1
  25. package/dist/features/triggers/utils.js +3 -3
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -4
  29. package/dist/monitoring/plugin.d.ts.map +1 -1
  30. package/dist/monitoring/plugin.js +31 -0
  31. package/dist/monitoring/routes/users.d.ts.map +1 -1
  32. package/dist/monitoring/routes/users.js +7 -6
  33. package/dist/monitoring/utils.d.ts.map +1 -1
  34. package/dist/monitoring/utils.js +5 -4
  35. package/dist/services/api/index.d.ts +4 -0
  36. package/dist/services/api/index.d.ts.map +1 -1
  37. package/dist/services/api/utils.d.ts +1 -0
  38. package/dist/services/api/utils.d.ts.map +1 -1
  39. package/dist/services/index.d.ts +4 -0
  40. package/dist/services/index.d.ts.map +1 -1
  41. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  42. package/dist/services/mongodb-atlas/index.js +9 -7
  43. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  44. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  45. package/dist/shared/handleUserDeletion.js +1 -1
  46. package/dist/shared/handleUserRegistration.js +2 -2
  47. package/dist/utils/context/helpers.d.ts +12 -0
  48. package/dist/utils/context/helpers.d.ts.map +1 -1
  49. package/dist/utils/index.d.ts +1 -0
  50. package/dist/utils/index.d.ts.map +1 -1
  51. package/dist/utils/index.js +14 -3
  52. package/dist/utils/initializer/exposeRoutes.js +1 -1
  53. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  54. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  55. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  56. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  57. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  58. package/dist/utils/initializer/registerPlugins.js +27 -5
  59. package/dist/utils/rules-matcher/interface.d.ts +5 -1
  60. package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
  61. package/dist/utils/rules-matcher/interface.js +2 -0
  62. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  63. package/dist/utils/rules-matcher/utils.js +51 -16
  64. package/package.json +4 -2
  65. package/src/auth/__tests__/controller.test.ts +1 -0
  66. package/src/auth/controller.ts +12 -11
  67. package/src/auth/plugins/jwt.ts +2 -2
  68. package/src/auth/providers/anon-user/__tests__/controller.test.ts +1 -0
  69. package/src/auth/providers/anon-user/controller.ts +2 -2
  70. package/src/auth/providers/custom-function/controller.ts +29 -8
  71. package/src/auth/providers/local-userpass/controller.ts +16 -15
  72. package/src/auth/utils.ts +1 -0
  73. package/src/constants.ts +14 -4
  74. package/src/features/encryption/interface.ts +46 -0
  75. package/src/features/encryption/utils.ts +22 -0
  76. package/src/features/rules/utils.ts +1 -11
  77. package/src/features/triggers/__tests__/index.test.ts +1 -0
  78. package/src/features/triggers/index.ts +6 -2
  79. package/src/features/triggers/utils.ts +4 -4
  80. package/src/index.ts +10 -2
  81. package/src/monitoring/plugin.ts +33 -0
  82. package/src/monitoring/routes/users.ts +8 -7
  83. package/src/monitoring/ui.collections.js +7 -10
  84. package/src/monitoring/ui.css +383 -1
  85. package/src/monitoring/ui.endpoints.js +5 -10
  86. package/src/monitoring/ui.events.js +4 -6
  87. package/src/monitoring/ui.functions.js +64 -71
  88. package/src/monitoring/ui.html +8 -0
  89. package/src/monitoring/ui.js +189 -0
  90. package/src/monitoring/ui.shared.js +239 -3
  91. package/src/monitoring/ui.triggers.js +2 -3
  92. package/src/monitoring/ui.users.js +5 -9
  93. package/src/monitoring/utils.ts +6 -5
  94. package/src/services/mongodb-atlas/index.ts +10 -13
  95. package/src/services/mongodb-atlas/model.ts +3 -1
  96. package/src/shared/handleUserDeletion.ts +2 -2
  97. package/src/shared/handleUserRegistration.ts +3 -3
  98. package/src/types/fastify-raw-body.d.ts +0 -9
  99. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  100. package/src/utils/__tests__/operators.test.ts +24 -0
  101. package/src/utils/__tests__/rule.test.ts +39 -0
  102. package/src/utils/__tests__/rulesMatcherInterfaces.test.ts +2 -0
  103. package/src/utils/index.ts +12 -1
  104. package/src/utils/initializer/exposeRoutes.ts +2 -2
  105. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  106. package/src/utils/initializer/registerPlugins.ts +45 -10
  107. package/src/utils/rules-matcher/interface.ts +5 -1
  108. package/src/utils/rules-matcher/utils.ts +78 -32
@@ -11,6 +11,7 @@ jest.mock('../../../../constants', () => ({
11
11
  'anon-user': { disabled: false }
12
12
  }
13
13
  },
14
+ AUTH_DB_NAME: 'test-auth-db',
14
15
  DB_NAME: 'test-db',
15
16
  DEFAULT_CONFIG: {
16
17
  REFRESH_TOKEN_TTL_DAYS: 1,
@@ -1,6 +1,6 @@
1
1
  import { ObjectId } from 'bson'
2
2
  import { FastifyInstance } from 'fastify'
3
- import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
3
+ import { AUTH_CONFIG, AUTH_DB_NAME, DEFAULT_CONFIG } from '../../../constants'
4
4
  import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
5
5
  import { hashToken } from '../../../utils/crypto'
6
6
  import { AUTH_ENDPOINTS } from '../../utils'
@@ -12,7 +12,7 @@ import { LoginDto } from './dtos'
12
12
  * @param {FastifyInstance} app - The Fastify instance.
13
13
  */
14
14
  export async function anonUserController(app: FastifyInstance) {
15
- const db = app.mongo.client.db(DB_NAME)
15
+ const db = app.mongo.client.db(AUTH_DB_NAME)
16
16
  const { authCollection, refreshTokensCollection } = AUTH_CONFIG
17
17
  const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
18
18
  const anonUserTtlSeconds = DEFAULT_CONFIG.ANON_USER_TTL_SECONDS
@@ -1,5 +1,6 @@
1
1
  import { FastifyInstance } from 'fastify'
2
- import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
2
+ import { ObjectId } from 'mongodb'
3
+ import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
3
4
  import { StateManager } from '../../../state'
4
5
  import { GenerateContext } from '../../../utils/context'
5
6
  import { hashToken } from '../../../utils/crypto'
@@ -16,7 +17,8 @@ export async function customFunctionController(app: FastifyInstance) {
16
17
 
17
18
  const functionsList = StateManager.select('functions')
18
19
  const services = StateManager.select('services')
19
- const db = app.mongo.client.db(DB_NAME)
20
+ const authDb = app.mongo.client.db(AUTH_DB_NAME)
21
+ const customUserDb = app.mongo.client.db(DB_NAME)
20
22
  const { authCollection, refreshTokensCollection, userCollection, user_id_field } = AUTH_CONFIG
21
23
  const refreshTokenTtlMs = DEFAULT_CONFIG.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000
22
24
 
@@ -54,7 +56,7 @@ export async function customFunctionController(app: FastifyInstance) {
54
56
  id
55
57
  } = req
56
58
 
57
- type CustomFunctionAuthResult = { id?: string }
59
+ type CustomFunctionAuthResult = { id?: string; email?: string }
58
60
  const authResult = await GenerateContext({
59
61
  args: [
60
62
  req.body
@@ -83,15 +85,34 @@ export async function customFunctionController(app: FastifyInstance) {
83
85
  return
84
86
  }
85
87
 
86
- const authUser = await db.collection(authCollection!).findOne({ email: authResult.id })
88
+ const email = authResult.email ?? authResult.id
89
+ let authUser = await authDb.collection(authCollection!).findOne({ email })
87
90
  if (!authUser) {
88
- reply.code(401).send({ message: 'Unauthorized' })
89
- return
91
+ const authUserId = new ObjectId()
92
+ await authDb.collection(authCollection!).insertOne({
93
+ _id: authUserId,
94
+ email,
95
+ status: 'confirmed',
96
+ createdAt: new Date(),
97
+ custom_data: {},
98
+ identities: [
99
+ {
100
+ id: authResult.id.toString(),
101
+ provider_id: authResult.id.toString(),
102
+ provider_type: 'custom-function',
103
+ provider_data: { email }
104
+ }
105
+ ]
106
+ })
107
+ authUser = {
108
+ _id: authUserId,
109
+ email
110
+ }
90
111
  }
91
112
 
92
113
  const user =
93
114
  user_id_field && userCollection
94
- ? await db
115
+ ? await customUserDb
95
116
  .collection(userCollection)
96
117
  .findOne({ [user_id_field]: authUser._id.toString() })
97
118
  : {}
@@ -109,7 +130,7 @@ export async function customFunctionController(app: FastifyInstance) {
109
130
  }
110
131
  const refreshToken = this.createRefreshToken(currentUserData)
111
132
  const refreshTokenHash = hashToken(refreshToken)
112
- await db.collection(refreshTokensCollection).insertOne({
133
+ await authDb.collection(refreshTokensCollection).insertOne({
113
134
  userId: authUser._id,
114
135
  tokenHash: refreshTokenHash,
115
136
  createdAt: new Date(),
@@ -1,6 +1,6 @@
1
1
  import { FastifyInstance } from 'fastify'
2
2
  import { ObjectId } from 'mongodb'
3
- import { AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
3
+ import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME, DEFAULT_CONFIG } from '../../../constants'
4
4
  import handleUserRegistration from '../../../shared/handleUserRegistration'
5
5
  import { PROVIDER } from '../../../shared/models/handleUserRegistration.model'
6
6
  import { StateManager } from '../../../state'
@@ -44,7 +44,8 @@ export async function localUserPassController(app: FastifyInstance) {
44
44
  const { authCollection, userCollection, user_id_field } = AUTH_CONFIG
45
45
  const { resetPasswordCollection } = AUTH_CONFIG
46
46
  const { refreshTokensCollection } = AUTH_CONFIG
47
- const db = app.mongo.client.db(DB_NAME)
47
+ const authDb = app.mongo.client.db(AUTH_DB_NAME)
48
+ const customUserDb = app.mongo.client.db(DB_NAME)
48
49
  const resetPasswordTtlSeconds = DEFAULT_CONFIG.RESET_PASSWORD_TTL_SECONDS
49
50
  const rateLimitWindowMs = DEFAULT_CONFIG.AUTH_RATE_LIMIT_WINDOW_MS
50
51
  const loginMaxAttempts = DEFAULT_CONFIG.AUTH_LOGIN_MAX_ATTEMPTS
@@ -54,7 +55,7 @@ export async function localUserPassController(app: FastifyInstance) {
54
55
  const resolveLocalUserpassProvider = () => AUTH_CONFIG.authProviders?.['local-userpass']
55
56
 
56
57
  try {
57
- await db.collection(resetPasswordCollection).createIndex(
58
+ await authDb.collection(resetPasswordCollection).createIndex(
58
59
  { createdAt: 1 },
59
60
  { expireAfterSeconds: resetPasswordTtlSeconds }
60
61
  )
@@ -63,7 +64,7 @@ export async function localUserPassController(app: FastifyInstance) {
63
64
  }
64
65
 
65
66
  try {
66
- await db.collection(refreshTokensCollection).createIndex(
67
+ await authDb.collection(refreshTokensCollection).createIndex(
67
68
  { expiresAt: 1 },
68
69
  { expireAfterSeconds: 0 }
69
70
  )
@@ -76,7 +77,7 @@ export async function localUserPassController(app: FastifyInstance) {
76
77
  extraArguments?: unknown[]
77
78
  ) => {
78
79
  const { resetPasswordConfig } = AUTH_CONFIG
79
- const authUser = await db.collection(authCollection!).findOne({
80
+ const authUser = await authDb.collection(authCollection!).findOne({
80
81
  email
81
82
  })
82
83
 
@@ -87,7 +88,7 @@ export async function localUserPassController(app: FastifyInstance) {
87
88
  const token = generateToken()
88
89
  const tokenId = generateToken()
89
90
 
90
- await db
91
+ await authDb
91
92
  ?.collection(resetPasswordCollection)
92
93
  .updateOne(
93
94
  { email },
@@ -194,7 +195,7 @@ export async function localUserPassController(app: FastifyInstance) {
194
195
  return
195
196
  }
196
197
 
197
- const existing = await db.collection(authCollection!).findOne({
198
+ const existing = await authDb.collection(authCollection!).findOne({
198
199
  confirmationToken: req.body.token,
199
200
  confirmationTokenId: req.body.tokenId
200
201
  }) as { _id: ObjectId; status?: string } | null
@@ -205,7 +206,7 @@ export async function localUserPassController(app: FastifyInstance) {
205
206
  }
206
207
 
207
208
  if (existing.status !== 'confirmed') {
208
- await db.collection(authCollection!).updateOne(
209
+ await authDb.collection(authCollection!).updateOne(
209
210
  { _id: existing._id },
210
211
  {
211
212
  $set: { status: 'confirmed' },
@@ -241,7 +242,7 @@ export async function localUserPassController(app: FastifyInstance) {
241
242
  res.status(429).send({ message: 'Too many requests' })
242
243
  return
243
244
  }
244
- const authUser = await db.collection(authCollection!).findOne({
245
+ const authUser = await authDb.collection(authCollection!).findOne({
245
246
  email: req.body.username
246
247
  })
247
248
 
@@ -260,7 +261,7 @@ export async function localUserPassController(app: FastifyInstance) {
260
261
 
261
262
  const user =
262
263
  user_id_field && userCollection
263
- ? await db!
264
+ ? await customUserDb
264
265
  .collection(userCollection)
265
266
  .findOne({ [user_id_field]: authUser._id.toString() })
266
267
  : {}
@@ -284,7 +285,7 @@ export async function localUserPassController(app: FastifyInstance) {
284
285
 
285
286
  const refreshToken = this.createRefreshToken(userWithCustomData)
286
287
  const refreshTokenHash = hashToken(refreshToken)
287
- await db.collection(refreshTokensCollection).insertOne({
288
+ await authDb.collection(refreshTokensCollection).insertOne({
288
289
  userId: authUser._id,
289
290
  tokenHash: refreshTokenHash,
290
291
  createdAt: new Date(),
@@ -382,7 +383,7 @@ export async function localUserPassController(app: FastifyInstance) {
382
383
  }
383
384
  const { token, tokenId, password } = req.body
384
385
 
385
- const resetRequest = await db
386
+ const resetRequest = await authDb
386
387
  ?.collection(resetPasswordCollection)
387
388
  .findOne({ token, tokenId })
388
389
 
@@ -396,11 +397,11 @@ export async function localUserPassController(app: FastifyInstance) {
396
397
  Date.now() - createdAt.getTime() > resetPasswordTtlSeconds * 1000
397
398
 
398
399
  if (isExpired) {
399
- await db?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
400
+ await authDb?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
400
401
  throw new Error(AUTH_ERRORS.INVALID_RESET_PARAMS)
401
402
  }
402
403
  const hashedPassword = await hashPassword(password)
403
- await db.collection(authCollection!).updateOne(
404
+ await authDb.collection(authCollection!).updateOne(
404
405
  { email: resetRequest.email },
405
406
  {
406
407
  $set: {
@@ -409,7 +410,7 @@ export async function localUserPassController(app: FastifyInstance) {
409
410
  }
410
411
  )
411
412
 
412
- await db?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
413
+ await authDb?.collection(resetPasswordCollection).deleteOne({ _id: resetRequest._id })
413
414
  }
414
415
  )
415
416
  }
package/src/auth/utils.ts CHANGED
@@ -122,6 +122,7 @@ export enum AUTH_ERRORS {
122
122
 
123
123
  export interface AuthConfig {
124
124
  auth_collection?: string
125
+ auth_database?: string
125
126
  'api-key': ApiKey
126
127
  'local-userpass': LocalUserpass
127
128
  'custom-function': CustomFunction
package/src/constants.ts CHANGED
@@ -14,12 +14,12 @@ const monitEnabled = typeof monitEnabledEnv === 'string'
14
14
  : false
15
15
 
16
16
  const {
17
- database_name,
17
+ database_name = 'main',
18
18
  collection_name = 'users',
19
19
  user_id_field = 'id',
20
20
  on_user_creation_function_name
21
21
  } = loadCustomUserData()
22
- const { auth_collection = 'auth_users', ...configuration } = loadAuthConfig()
22
+ const { auth_collection = 'auth_users', auth_database, ...configuration } = loadAuthConfig()
23
23
 
24
24
  export const DEFAULT_CONFIG = {
25
25
  PORT: Number(process.env.PORT) || 3000,
@@ -57,11 +57,16 @@ export const DEFAULT_CONFIG = {
57
57
  CORS_OPTIONS: {
58
58
  origin: "*",
59
59
  methods: ["GET", "POST", "PUT", "DELETE"] as ALLOWED_METHODS[]
60
+ },
61
+ MONGODB_ENCRYPTION_CONFIG: {
62
+ keyVaultDb: "encryption",
63
+ keyVaultCollection: "__keyVault"
60
64
  }
61
65
  }
62
66
  export const API_VERSION = `/api/client/${DEFAULT_CONFIG.API_VERSION}`
63
67
  export const HTTPS_SCHEMA = DEFAULT_CONFIG.HTTPS_SCHEMA
64
68
  export const DB_NAME = database_name
69
+ export const AUTH_DB_NAME = auth_database ?? database_name
65
70
 
66
71
  type AuthProviders = Record<string, { disabled?: boolean; config?: unknown }>
67
72
  // TODO spostare nell'oggetto providers anche le altre configurazioni
@@ -81,9 +86,14 @@ export const AUTH_CONFIG = {
81
86
  }
82
87
  }
83
88
 
84
-
85
-
86
89
  export const S3_CONFIG = {
87
90
  ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
88
91
  SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY
89
92
  }
93
+
94
+ /**
95
+ * Name of the MongoDB client to use for change streams.
96
+ * This may be a separate instance because streams do not work
97
+ * when the main client has auto encryption enabled.
98
+ */
99
+ export const CHANGESTREAM = "changestream"
@@ -0,0 +1,46 @@
1
+ import type { UUID } from "mongodb"
2
+
3
+ export type EncryptionSchemaProperty =
4
+ | EncryptionSchema
5
+ | {
6
+ encrypt: {
7
+ algorithm: string
8
+ bsonType: string
9
+ keyAlias?: string
10
+ }
11
+ }
12
+
13
+ export type EncryptionSchema = {
14
+ bsonType: "object"
15
+ properties: Record<string, EncryptionSchemaProperty>
16
+ encryptMetadata?: {
17
+ keyAlias: string
18
+ },
19
+ }
20
+
21
+
22
+ export type MappedEncryptionSchemaProperty =
23
+ | MappedEncryptionSchema
24
+ | {
25
+ encrypt: {
26
+ algorithm: string
27
+ bsonType: string
28
+ keyId?: [UUID]
29
+ }
30
+ }
31
+
32
+ export type MappedEncryptionSchema = {
33
+ bsonType: "object"
34
+ properties: Record<string, MappedEncryptionSchemaProperty>
35
+ encryptMetadata?: {
36
+ keyId: [UUID]
37
+ },
38
+ }
39
+
40
+ export type EncryptionSchemaFile = {
41
+ database: string
42
+ collection: string
43
+ schema: EncryptionSchema
44
+ }
45
+
46
+ export type EncryptionSchemas = Record<string, EncryptionSchema>
@@ -0,0 +1,22 @@
1
+ import path from "node:path"
2
+ import { readJsonContent, recursivelyCollectFiles } from "../../utils"
3
+ import { EncryptionSchemaFile, EncryptionSchemas } from "./interface"
4
+
5
+ /**
6
+ * @experimental
7
+ * Schemas used for Client-Side Level Encryption configuration.
8
+ *
9
+ * **Important:** These schemas do not perform JSON validation.
10
+ */
11
+ export const loadEncryptionSchemas = async (rootDir = process.cwd()): Promise<EncryptionSchemas> => {
12
+ const schemasRoot = path.join(rootDir, 'data_sources', 'mongodb-atlas')
13
+
14
+ const files = recursivelyCollectFiles(schemasRoot)
15
+ const schemaFiles = files.filter((x) => x.endsWith('encryption.json'))
16
+
17
+ return schemaFiles.reduce((acc, filePath) => {
18
+ const { collection, database, schema } = readJsonContent(filePath) as EncryptionSchemaFile
19
+ acc[`${database}.${collection}`] = schema
20
+ return acc
21
+ }, {} as EncryptionSchemas)
22
+ }
@@ -1,19 +1,9 @@
1
- import fs from 'fs'
2
1
  import path from 'node:path'
3
- import { readJsonContent } from '../../utils'
2
+ import { readJsonContent, recursivelyCollectFiles } from '../../utils'
4
3
  import { Rules, RulesConfig } from './interface'
5
4
 
6
5
  export const loadRules = async (rootDir = process.cwd()): Promise<Rules> => {
7
6
  const rulesRoot = path.join(rootDir, 'data_sources', 'mongodb-atlas')
8
- const recursivelyCollectFiles = (dir: string): string[] => {
9
- return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
10
- const fullPath = path.join(dir, entry.name)
11
- if (entry.isDirectory()) {
12
- return recursivelyCollectFiles(fullPath)
13
- }
14
- return entry.isFile() ? [fullPath] : []
15
- })
16
- }
17
7
  const files = recursivelyCollectFiles(rulesRoot)
18
8
  const rulesFiles = files.filter((x) => (x as string).endsWith('rules.json'))
19
9
 
@@ -3,6 +3,7 @@ import { TRIGGER_HANDLERS } from '../utils'
3
3
 
4
4
  jest.mock('../../../constants', () => ({
5
5
  AUTH_CONFIG: {},
6
+ AUTH_DB_NAME: 'test-auth-db',
6
7
  DB_NAME: 'test-db'
7
8
  }))
8
9
 
@@ -1,4 +1,4 @@
1
- import { AUTH_CONFIG, DB_NAME } from '../../constants'
1
+ import { AUTH_CONFIG, AUTH_DB_NAME, CHANGESTREAM } from '../../constants'
2
2
  import { services } from '../../services'
3
3
  import { Function, Functions } from '../functions/interface'
4
4
  import { ActivateTriggersParams } from './dtos'
@@ -18,6 +18,10 @@ export const activateTriggers = async ({
18
18
  }: ActivateTriggersParams) => {
19
19
  console.log('START ACTIVATION TRIGGERS')
20
20
  try {
21
+ // Ensure the changestream MongoDB client exist, or use the main client
22
+ if (!fastify.mongo[CHANGESTREAM]) {
23
+ fastify.mongo[CHANGESTREAM] = fastify.mongo
24
+ }
21
25
  const triggersToActivate = [...triggersList]
22
26
  if (AUTH_CONFIG.on_user_creation_function_name) {
23
27
  const alreadyDeclared = triggersToActivate.some(
@@ -36,7 +40,7 @@ export const activateTriggers = async ({
36
40
  config: {
37
41
  isAutoTrigger: true,
38
42
  collection: AUTH_CONFIG.authCollection ?? 'auth_users',
39
- database: DB_NAME,
43
+ database: AUTH_DB_NAME,
40
44
  full_document: true,
41
45
  full_document_before_change: false,
42
46
  match: {},
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'node:path'
3
3
  import cron from 'node-cron'
4
- import { AUTH_CONFIG, DB_NAME } from '../../constants'
4
+ import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME, CHANGESTREAM } from '../../constants'
5
5
  import { createEventId, sanitize } from '../../monitoring/utils'
6
6
  import { StateManager } from '../../state'
7
7
  import { readJsonContent } from '../../utils'
@@ -246,11 +246,11 @@ const handleAuthenticationTrigger = async ({
246
246
  const { database, isAutoTrigger, operation_types = [], operation_type } = config
247
247
  const providerFilter = normalizeProviders(config.providers ?? [])
248
248
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
249
- const collection = app.mongo.client.db(database || DB_NAME).collection(authCollection)
249
+ const collection = app.mongo[CHANGESTREAM].client.db(database || AUTH_DB_NAME).collection(authCollection)
250
250
  const operationCandidates = operation_type ? mapOpInverse[operation_type] : operation_types
251
251
  const normalizedOps = normalizeOperationTypes(operationCandidates)
252
252
  const baseMeta = {
253
- database: database || DB_NAME,
253
+ database: database || AUTH_DB_NAME,
254
254
  collection: authCollection,
255
255
  operationTypes: normalizedOps,
256
256
  providers: providerFilter,
@@ -652,7 +652,7 @@ const handleDataBaseTrigger = async ({
652
652
 
653
653
  const normalizedOperations = normalizeOperationTypes(operation_types)
654
654
 
655
- const collection = app.mongo.client.db(database).collection(collectionName)
655
+ const collection = app.mongo[CHANGESTREAM].client.db(database).collection(collectionName)
656
656
  const pipeline = [
657
657
  {
658
658
  $match: {
package/src/index.ts CHANGED
@@ -6,12 +6,14 @@ import { loadEndpoints } from './features/endpoints/utils'
6
6
  import { registerFunctions } from './features/functions'
7
7
  import { loadFunctions } from './features/functions/utils'
8
8
  import { loadRules } from './features/rules/utils'
9
+ import { loadEncryptionSchemas } from './features/encryption/utils'
9
10
  import { activateTriggers } from './features/triggers'
10
11
  import { loadTriggers } from './features/triggers/utils'
11
12
  import { services } from './services'
12
13
  import { StateManager } from './state'
13
14
  import { exposeRoutes } from './utils/initializer/exposeRoutes'
14
15
  import { registerPlugins } from './utils/initializer/registerPlugins'
16
+ import { type MongoDbEncryptionConfig } from './utils/initializer/mongodbCSFLE'
15
17
  export * from './model'
16
18
 
17
19
 
@@ -30,6 +32,7 @@ export type InitializeConfig = {
30
32
  host?: string
31
33
  corsConfig?: CorsConfig
32
34
  basePath?: string
35
+ mongodbEncryptionConfig?: MongoDbEncryptionConfig
33
36
  }
34
37
 
35
38
  /**
@@ -47,7 +50,8 @@ export async function initialize({
47
50
  port = DEFAULT_CONFIG.PORT,
48
51
  mongodbUrl = DEFAULT_CONFIG.MONGODB_URL,
49
52
  corsConfig = DEFAULT_CONFIG.CORS_OPTIONS,
50
- basePath
53
+ basePath,
54
+ mongodbEncryptionConfig
51
55
  }: InitializeConfig) {
52
56
  if (!jwtSecret || jwtSecret.trim().length === 0) {
53
57
  throw new Error('JWT secret missing: set JWT_SECRET or pass jwtSecret to initialize()')
@@ -77,6 +81,8 @@ export async function initialize({
77
81
  logInfo("Endpoints LOADED")
78
82
  const rulesList = await loadRules(resolvedBasePath)
79
83
  logInfo("Rules LOADED")
84
+ const encryptionSchemas = await loadEncryptionSchemas(resolvedBasePath)
85
+ logInfo("Encryption schemas LOADED")
80
86
 
81
87
  const stateConfig = {
82
88
  functions: functionsList,
@@ -152,7 +158,9 @@ export async function initialize({
152
158
  mongodbUrl,
153
159
  jwtSecret,
154
160
  functionsList,
155
- corsConfig
161
+ corsConfig,
162
+ encryptionSchemas,
163
+ mongodbEncryptionConfig
156
164
  })
157
165
 
158
166
  logInfo('Plugins registration COMPLETED')
@@ -2,6 +2,7 @@ import fastifyWebsocket from '@fastify/websocket'
2
2
  import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
3
3
  import fp from 'fastify-plugin'
4
4
  import '@fastify/websocket'
5
+ import fs from 'fs'
5
6
  import { DEFAULT_CONFIG } from '../constants'
6
7
  import { StateManager } from '../state'
7
8
  import { registerCollectionRoutes } from './routes/collections'
@@ -316,6 +317,38 @@ const createMonitoringPlugin = fp(async (
316
317
  })
317
318
  })
318
319
 
320
+ const resolveCodeMirrorAsset = (internalPath: string) => {
321
+ try {
322
+ return require.resolve(`codemirror/${internalPath}`)
323
+ } catch {
324
+ return ''
325
+ }
326
+ }
327
+
328
+ const codemirrorAssets: Record<string, string> = {
329
+ 'codemirror.js': resolveCodeMirrorAsset('lib/codemirror.js'),
330
+ 'codemirror.css': resolveCodeMirrorAsset('lib/codemirror.css'),
331
+ 'javascript.js': resolveCodeMirrorAsset('mode/javascript/javascript.js'),
332
+ 'foldcode.js': resolveCodeMirrorAsset('addon/fold/foldcode.js'),
333
+ 'foldgutter.js': resolveCodeMirrorAsset('addon/fold/foldgutter.js'),
334
+ 'brace-fold.js': resolveCodeMirrorAsset('addon/fold/brace-fold.js'),
335
+ 'comment-fold.js': resolveCodeMirrorAsset('addon/fold/comment-fold.js'),
336
+ 'foldgutter.css': resolveCodeMirrorAsset('addon/fold/foldgutter.css')
337
+ }
338
+
339
+ Object.entries(codemirrorAssets).forEach(([assetName, relativePath]) => {
340
+ app.get(`${prefix}/vendor/codemirror/${assetName}`, async (_req, reply) => {
341
+ const assetPath = relativePath || ''
342
+ if (!assetPath || !fs.existsSync(assetPath)) {
343
+ reply.code(404).send(`${assetName} not found`)
344
+ return
345
+ }
346
+ const asset = fs.readFileSync(assetPath, 'utf8')
347
+ reply.header('Cache-Control', 'no-store')
348
+ reply.type(assetName.endsWith('.css') ? 'text/css' : 'application/javascript').send(asset)
349
+ })
350
+ })
351
+
319
352
  app.get(`${prefix}/ws`, { websocket: true }, (connection) => {
320
353
  const socket =
321
354
  (connection as {
@@ -1,7 +1,7 @@
1
1
  import type { FastifyInstance } from 'fastify'
2
2
  import { ObjectId } from 'mongodb'
3
3
  import { loadAuthConfig, loadCustomUserData, PASSWORD_RULES } from '../../auth/utils'
4
- import { AUTH_CONFIG, DB_NAME } from '../../constants'
4
+ import { AUTH_CONFIG, AUTH_DB_NAME, DB_NAME } from '../../constants'
5
5
  import handleUserRegistration from '../../shared/handleUserRegistration'
6
6
  import { PROVIDER } from '../../shared/models/handleUserRegistration.model'
7
7
  import { hashPassword } from '../../utils/crypto'
@@ -47,7 +47,8 @@ export const registerUserRoutes = (app: FastifyInstance, deps: UserRoutesDeps) =
47
47
  const resolvedAuthLimit = Math.min(Number.isFinite(parsedAuthLimit) && parsedAuthLimit > 0 ? parsedAuthLimit : 100, 500)
48
48
  const resolvedCustomLimit = Math.min(Number.isFinite(parsedCustomLimit) && parsedCustomLimit > 0 ? parsedCustomLimit : 25, 500)
49
49
  const resolvedCustomPage = Math.max(Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1, 1)
50
- const db = app.mongo.client.db(DB_NAME)
50
+ const authDb = app.mongo.client.db(AUTH_DB_NAME)
51
+ const customDb = app.mongo.client.db(DB_NAME)
51
52
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
52
53
  const userCollection = AUTH_CONFIG.userCollection
53
54
 
@@ -69,7 +70,7 @@ export const registerUserRoutes = (app: FastifyInstance, deps: UserRoutesDeps) =
69
70
  ]
70
71
  }
71
72
  : {}
72
- const authItems = await db
73
+ const authItems = await authDb
73
74
  .collection(authCollection)
74
75
  .find(authFilter)
75
76
  .sort({ createdAt: -1, _id: -1 })
@@ -94,11 +95,11 @@ export const registerUserRoutes = (app: FastifyInstance, deps: UserRoutesDeps) =
94
95
  ]
95
96
  }
96
97
  : {}
97
- const total = await db.collection(userCollection).countDocuments(customFilter)
98
+ const total = await customDb.collection(userCollection).countDocuments(customFilter)
98
99
  const totalPages = Math.max(1, Math.ceil(total / Math.max(resolvedCustomLimit, 1)))
99
100
  const page = Math.min(resolvedCustomPage, totalPages)
100
101
  const skip = Math.max(0, (page - 1) * resolvedCustomLimit)
101
- const customItems = await db
102
+ const customItems = await customDb
102
103
  .collection(userCollection)
103
104
  .find(customFilter)
104
105
  .sort({ createdAt: -1, _id: -1 })
@@ -193,7 +194,7 @@ export const registerUserRoutes = (app: FastifyInstance, deps: UserRoutesDeps) =
193
194
  return { error: passwordError }
194
195
  }
195
196
 
196
- const db = app.mongo.client.db(DB_NAME)
197
+ const db = app.mongo.client.db(AUTH_DB_NAME)
197
198
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
198
199
  const selector: Record<string, unknown> = {}
199
200
 
@@ -231,7 +232,7 @@ export const registerUserRoutes = (app: FastifyInstance, deps: UserRoutesDeps) =
231
232
  app.patch(`${prefix}/api/users/:id/status`, async (req, reply) => {
232
233
  const params = req.params as { id: string }
233
234
  const body = req.body as { disabled?: boolean; status?: string; email?: string }
234
- const db = app.mongo.client.db(DB_NAME)
235
+ const db = app.mongo.client.db(AUTH_DB_NAME)
235
236
  const authCollection = AUTH_CONFIG.authCollection ?? 'auth_users'
236
237
  const selector: Record<string, unknown> = {}
237
238
 
@@ -91,7 +91,7 @@
91
91
  collectionTabButtons,
92
92
  collectionTabPanels
93
93
  } = dom;
94
- const { api, parseJsonObject, highlightJson, safeStringify } = utils;
94
+ const { api, parseJsonObject, highlightJson, renderJsonViewer, clearJsonViewer, safeStringify } = utils;
95
95
 
96
96
  const TABLE_TRUNCATE_LIMIT = 200;
97
97
 
@@ -337,22 +337,21 @@
337
337
  const highlight = state.collectionResultHighlight;
338
338
  if (payload === null || payload === undefined) {
339
339
  collectionResult.classList.remove('table-view', 'json-highlight');
340
- collectionResult.textContent = '';
340
+ clearJsonViewer(collectionResult, '');
341
341
  return;
342
342
  }
343
343
  if (state.collectionResultView === 'table') {
344
+ clearJsonViewer(collectionResult, '');
344
345
  renderCollectionTable(payload);
345
346
  return;
346
347
  }
347
348
  collectionResult.classList.remove('table-view');
348
349
  if (highlight) {
349
350
  const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
350
- collectionResult.classList.add('json-highlight');
351
- collectionResult.innerHTML = highlightJson(text || '');
351
+ renderJsonViewer(collectionResult, text || '', { collapsible: true });
352
352
  return;
353
353
  }
354
- collectionResult.classList.remove('json-highlight');
355
- collectionResult.textContent = typeof payload === 'string' ? payload : String(payload ?? '');
354
+ clearJsonViewer(collectionResult, typeof payload === 'string' ? payload : String(payload ?? ''));
356
355
  };
357
356
 
358
357
  const setCollectionResult = (value, highlight) => {
@@ -365,12 +364,10 @@
365
364
  if (!collectionRules) return;
366
365
  if (highlight) {
367
366
  const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
368
- collectionRules.classList.add('json-highlight');
369
- collectionRules.innerHTML = highlightJson(text || '');
367
+ renderJsonViewer(collectionRules, text || '', { collapsible: true });
370
368
  return;
371
369
  }
372
- collectionRules.classList.remove('json-highlight');
373
- collectionRules.textContent = typeof value === 'string' ? value : String(value ?? '');
370
+ clearJsonViewer(collectionRules, typeof value === 'string' ? value : String(value ?? ''));
374
371
  };
375
372
 
376
373
  const updateCollectionModeView = () => {