@flowerforce/flowerbase 1.1.2-beta.9 → 1.2.1-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.
- package/CHANGELOG.md +12 -0
- package/README.md +28 -3
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +57 -3
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/auth/plugins/jwt.js +49 -3
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +19 -3
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +125 -71
- package/dist/auth/providers/local-userpass/dtos.d.ts +11 -2
- package/dist/auth/providers/local-userpass/dtos.d.ts.map +1 -1
- package/dist/auth/utils.d.ts +53 -14
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/auth/utils.js +46 -63
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -2
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +52 -11
- package/dist/features/functions/dtos.d.ts +3 -0
- package/dist/features/functions/dtos.d.ts.map +1 -1
- package/dist/features/functions/interface.d.ts +3 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.d.ts +2 -1
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +19 -7
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +11 -2
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +48 -7
- package/dist/features/triggers/utils.d.ts.map +1 -1
- package/dist/features/triggers/utils.js +118 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -21
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +605 -478
- package/dist/services/mongodb-atlas/model.d.ts +2 -1
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.d.ts +9 -2
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +113 -23
- package/dist/shared/handleUserRegistration.d.ts.map +1 -1
- package/dist/shared/handleUserRegistration.js +1 -0
- package/dist/shared/models/handleUserRegistration.model.d.ts +6 -2
- package/dist/shared/models/handleUserRegistration.model.d.ts.map +1 -1
- package/dist/utils/context/helpers.d.ts +7 -6
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +3 -0
- package/dist/utils/context/index.d.ts +1 -1
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +176 -5
- package/dist/utils/context/interface.d.ts +1 -1
- package/dist/utils/context/interface.d.ts.map +1 -1
- package/dist/utils/crypto/index.d.ts +1 -0
- package/dist/utils/crypto/index.d.ts.map +1 -1
- package/dist/utils/crypto/index.js +6 -2
- package/dist/utils/initializer/exposeRoutes.d.ts.map +1 -1
- package/dist/utils/initializer/exposeRoutes.js +11 -4
- package/dist/utils/initializer/registerPlugins.d.ts +3 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +9 -6
- package/dist/utils/roles/helpers.js +11 -3
- package/dist/utils/roles/machines/commonValidators.d.ts.map +1 -1
- package/dist/utils/roles/machines/commonValidators.js +10 -6
- package/dist/utils/roles/machines/read/B/validators.d.ts +4 -0
- package/dist/utils/roles/machines/read/B/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/B/validators.js +8 -0
- package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/C/index.js +10 -7
- package/dist/utils/roles/machines/read/C/validators.d.ts +5 -0
- package/dist/utils/roles/machines/read/C/validators.d.ts.map +1 -0
- package/dist/utils/roles/machines/read/C/validators.js +29 -0
- package/dist/utils/roles/machines/read/D/index.d.ts.map +1 -1
- package/dist/utils/roles/machines/read/D/index.js +13 -11
- package/dist/utils/rules.d.ts +1 -1
- package/dist/utils/rules.d.ts.map +1 -1
- package/dist/utils/rules.js +26 -17
- package/jest.config.ts +2 -12
- package/jest.setup.ts +28 -0
- package/package.json +1 -2
- package/src/auth/controller.ts +70 -4
- package/src/auth/plugins/jwt.test.ts +93 -0
- package/src/auth/plugins/jwt.ts +62 -3
- package/src/auth/providers/custom-function/controller.ts +22 -5
- package/src/auth/providers/local-userpass/controller.ts +168 -96
- package/src/auth/providers/local-userpass/dtos.ts +13 -2
- package/src/auth/utils.ts +51 -86
- package/src/constants.ts +16 -3
- package/src/fastify.d.ts +32 -15
- package/src/features/functions/controller.ts +81 -14
- package/src/features/functions/dtos.ts +3 -0
- package/src/features/functions/interface.ts +3 -0
- package/src/features/functions/utils.ts +29 -8
- package/src/features/rules/utils.ts +11 -2
- package/src/features/triggers/index.ts +43 -1
- package/src/features/triggers/utils.ts +146 -38
- package/src/index.ts +69 -20
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +95 -0
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +141 -0
- package/src/services/mongodb-atlas/index.ts +241 -90
- package/src/services/mongodb-atlas/model.ts +15 -2
- package/src/services/mongodb-atlas/utils.ts +158 -22
- package/src/shared/handleUserRegistration.ts +3 -3
- package/src/shared/models/handleUserRegistration.model.ts +8 -3
- package/src/types/fastify-raw-body.d.ts +22 -0
- package/src/utils/__tests__/STEP_B_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_C_STATES.test.ts +1 -1
- package/src/utils/__tests__/STEP_D_STATES.test.ts +2 -2
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +9 -4
- package/src/utils/__tests__/registerPlugins.test.ts +16 -1
- package/src/utils/context/helpers.ts +3 -0
- package/src/utils/context/index.ts +238 -13
- package/src/utils/context/interface.ts +1 -1
- package/src/utils/crypto/index.ts +5 -1
- package/src/utils/initializer/exposeRoutes.ts +15 -8
- package/src/utils/initializer/registerPlugins.ts +15 -7
- package/src/utils/roles/helpers.ts +23 -5
- package/src/utils/roles/machines/commonValidators.ts +10 -5
- package/src/utils/roles/machines/read/B/validators.ts +8 -0
- package/src/utils/roles/machines/read/C/index.ts +11 -7
- package/src/utils/roles/machines/read/C/validators.ts +21 -0
- package/src/utils/roles/machines/read/D/index.ts +22 -12
- package/src/utils/rules.ts +31 -22
- package/tsconfig.spec.json +7 -0
package/src/auth/utils.ts
CHANGED
|
@@ -8,19 +8,45 @@ export const LOGIN_SCHEMA = {
|
|
|
8
8
|
body: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
username: {
|
|
12
|
-
|
|
11
|
+
username: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
14
|
+
minLength: 3,
|
|
15
|
+
maxLength: 254
|
|
16
|
+
},
|
|
17
|
+
password: { type: 'string', minLength: 8, maxLength: 128 }
|
|
13
18
|
},
|
|
14
19
|
required: ['username', 'password']
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
export const
|
|
23
|
+
export const RESET_SEND_SCHEMA = {
|
|
19
24
|
body: {
|
|
20
25
|
type: 'object',
|
|
21
26
|
properties: {
|
|
22
|
-
email: {
|
|
23
|
-
|
|
27
|
+
email: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
30
|
+
minLength: 3,
|
|
31
|
+
maxLength: 254
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ['email']
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const RESET_CALL_SCHEMA = {
|
|
39
|
+
body: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
email: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
45
|
+
minLength: 3,
|
|
46
|
+
maxLength: 254
|
|
47
|
+
},
|
|
48
|
+
password: { type: 'string', minLength: 8, maxLength: 128 },
|
|
49
|
+
arguments: { type: 'array' }
|
|
24
50
|
},
|
|
25
51
|
required: ['email', 'password']
|
|
26
52
|
}
|
|
@@ -30,7 +56,7 @@ export const CONFIRM_RESET_SCHEMA = {
|
|
|
30
56
|
body: {
|
|
31
57
|
type: 'object',
|
|
32
58
|
properties: {
|
|
33
|
-
password: { type: 'string' },
|
|
59
|
+
password: { type: 'string', minLength: 8, maxLength: 128 },
|
|
34
60
|
token: { type: 'string' },
|
|
35
61
|
tokenId: { type: 'string' }
|
|
36
62
|
},
|
|
@@ -38,12 +64,19 @@ export const CONFIRM_RESET_SCHEMA = {
|
|
|
38
64
|
}
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
export const RESET_SCHEMA = RESET_SEND_SCHEMA
|
|
68
|
+
|
|
41
69
|
export const REGISTRATION_SCHEMA = {
|
|
42
70
|
body: {
|
|
43
71
|
type: 'object',
|
|
44
72
|
properties: {
|
|
45
|
-
email: {
|
|
46
|
-
|
|
73
|
+
email: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$',
|
|
76
|
+
minLength: 3,
|
|
77
|
+
maxLength: 254
|
|
78
|
+
},
|
|
79
|
+
password: { type: 'string', minLength: 8, maxLength: 128 }
|
|
47
80
|
},
|
|
48
81
|
required: ['email', 'password']
|
|
49
82
|
}
|
|
@@ -54,7 +87,8 @@ export enum AUTH_ENDPOINTS {
|
|
|
54
87
|
REGISTRATION = '/register',
|
|
55
88
|
PROFILE = '/profile',
|
|
56
89
|
SESSION = '/session',
|
|
57
|
-
RESET = '/reset/
|
|
90
|
+
RESET = '/reset/send',
|
|
91
|
+
RESET_CALL = '/reset/call',
|
|
58
92
|
CONFIRM_RESET = "/reset",
|
|
59
93
|
FIRST_USER = '/setup/first-user'
|
|
60
94
|
}
|
|
@@ -62,7 +96,8 @@ export enum AUTH_ENDPOINTS {
|
|
|
62
96
|
export enum AUTH_ERRORS {
|
|
63
97
|
INVALID_CREDENTIALS = 'Invalid credentials',
|
|
64
98
|
INVALID_TOKEN = 'Invalid refresh token provided',
|
|
65
|
-
INVALID_RESET_PARAMS = 'Invalid token or tokenId provided'
|
|
99
|
+
INVALID_RESET_PARAMS = 'Invalid token or tokenId provided',
|
|
100
|
+
MISSING_RESET_FUNCTION = 'Missing reset function'
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
export interface AuthConfig {
|
|
@@ -99,11 +134,6 @@ export interface Config {
|
|
|
99
134
|
resetPasswordUrl: string
|
|
100
135
|
runConfirmationFunction: boolean
|
|
101
136
|
runResetFunction: boolean
|
|
102
|
-
mailConfig: {
|
|
103
|
-
from: string
|
|
104
|
-
subject: string
|
|
105
|
-
mailToken: string
|
|
106
|
-
}
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
export interface CustomUserDataConfig {
|
|
@@ -115,13 +145,16 @@ export interface CustomUserDataConfig {
|
|
|
115
145
|
on_user_creation_function_name: string
|
|
116
146
|
}
|
|
117
147
|
|
|
148
|
+
const resolveAppPath = () =>
|
|
149
|
+
process.env.FLOWERBASE_APP_PATH ?? require.main?.path ?? process.cwd()
|
|
150
|
+
|
|
118
151
|
|
|
119
152
|
/**
|
|
120
153
|
* > Loads the auth config json file
|
|
121
154
|
* @testable
|
|
122
155
|
*/
|
|
123
156
|
export const loadAuthConfig = (): AuthConfig => {
|
|
124
|
-
const authPath = path.join(
|
|
157
|
+
const authPath = path.join(resolveAppPath(), 'auth/providers.json')
|
|
125
158
|
return JSON.parse(fs.readFileSync(authPath, 'utf-8'))
|
|
126
159
|
}
|
|
127
160
|
|
|
@@ -130,79 +163,11 @@ export const loadAuthConfig = (): AuthConfig => {
|
|
|
130
163
|
* @testable
|
|
131
164
|
*/
|
|
132
165
|
export const loadCustomUserData = (): CustomUserDataConfig => {
|
|
133
|
-
const userDataPath = path.join(
|
|
166
|
+
const userDataPath = path.join(resolveAppPath(), 'auth/custom_user_data.json')
|
|
134
167
|
return JSON.parse(fs.readFileSync(userDataPath, 'utf-8'))
|
|
135
168
|
}
|
|
136
169
|
|
|
137
|
-
export const getMailConfig = (
|
|
138
|
-
resetPasswordConfig: Config,
|
|
139
|
-
token: string,
|
|
140
|
-
tokenId: string
|
|
141
|
-
) => {
|
|
142
|
-
const { mailConfig, resetPasswordUrl } = resetPasswordConfig
|
|
143
|
-
const ENV_PREFIX = 'ENV'
|
|
144
|
-
const { from, subject, mailToken } = mailConfig
|
|
145
|
-
|
|
146
|
-
const [fromPrefix, fromPath] = from.split('.')
|
|
147
|
-
|
|
148
|
-
if (!fromPath) {
|
|
149
|
-
throw new Error(`Invalid fromPath: ${fromPath}`)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const currentSender = (fromPrefix === ENV_PREFIX ? process.env[fromPath] : from) ?? ''
|
|
153
|
-
const [subjectPrefix, subjectPath] = subject.split('.')
|
|
154
|
-
|
|
155
|
-
if (!subjectPath) {
|
|
156
|
-
throw new Error(`Invalid subjectPath: ${subjectPath}`)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const currentSubject =
|
|
160
|
-
(subjectPrefix === ENV_PREFIX ? process.env[subjectPath] : subject) ?? ''
|
|
161
|
-
const [mailTokenPrefix, mailTokenPath] = mailToken.split('.')
|
|
162
|
-
|
|
163
|
-
if (!mailTokenPath) {
|
|
164
|
-
throw new Error(`Invalid mailTokenPath: ${mailTokenPath}`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const currentMailToken =
|
|
168
|
-
(mailTokenPrefix === 'ENV' ? process.env[mailTokenPath] : mailToken) ?? ''
|
|
169
|
-
|
|
170
|
-
const link = `${resetPasswordUrl}/${token}/${tokenId}`
|
|
171
|
-
const body = `<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; text-align: center; padding: 20px;">
|
|
172
|
-
<table width="100%" cellspacing="0" cellpadding="0">
|
|
173
|
-
<tr>
|
|
174
|
-
<td align="center">
|
|
175
|
-
<table width="600" cellspacing="0" cellpadding="0" style="background: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
|
|
176
|
-
<tr>
|
|
177
|
-
<td align="center">
|
|
178
|
-
<h2>Password Reset Request</h2>
|
|
179
|
-
<p>If you requested a password reset, click the button below to reset your password.</p>
|
|
180
|
-
<p>If you did not request this, please ignore this email.</p>
|
|
181
|
-
<p>
|
|
182
|
-
<a href="${link}" style="display: inline-block; padding: 12px 20px; font-size: 16px; color: #ffffff; background: #007bff; text-decoration: none; border-radius: 5px;">Reset Password</a>
|
|
183
|
-
</p>
|
|
184
|
-
<p style="margin-top: 20px; font-size: 12px; color: #777;">If the button does not work, copy and paste the following link into your browser:</p>
|
|
185
|
-
<p style="font-size: 12px; color: #777;">${link}</p>
|
|
186
|
-
</td>
|
|
187
|
-
</tr>
|
|
188
|
-
</table>
|
|
189
|
-
</td>
|
|
190
|
-
</tr>
|
|
191
|
-
</table>
|
|
192
|
-
</body>`
|
|
193
|
-
return {
|
|
194
|
-
from: currentSender ?? '',
|
|
195
|
-
subject: currentSubject,
|
|
196
|
-
mailToken: currentMailToken,
|
|
197
|
-
body
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
170
|
export const generatePassword = (length = 20) => {
|
|
206
171
|
const bytes = crypto.randomBytes(length);
|
|
207
172
|
return Array.from(bytes, (b) => CHARSET[b % CHARSET.length]).join("");
|
|
208
|
-
}
|
|
173
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAuthConfig, loadCustomUserData } from './auth/utils'
|
|
2
|
+
import { ALLOWED_METHODS } from './'
|
|
2
3
|
|
|
3
4
|
const {
|
|
4
5
|
database_name,
|
|
@@ -15,7 +16,18 @@ export const DEFAULT_CONFIG = {
|
|
|
15
16
|
API_VERSION: process.env.API_VERSION || 'v2.0',
|
|
16
17
|
HTTPS_SCHEMA: process.env.HTTPS_SCHEMA || 'https',
|
|
17
18
|
HOST: process.env.HOST || '0.0.0.0',
|
|
18
|
-
ENABLE_LOGGER: process.env.ENABLE_LOGGER
|
|
19
|
+
ENABLE_LOGGER: process.env.ENABLE_LOGGER,
|
|
20
|
+
RESET_PASSWORD_TTL_SECONDS: Number(process.env.RESET_PASSWORD_TTL_SECONDS) || 3600,
|
|
21
|
+
AUTH_RATE_LIMIT_WINDOW_MS: Number(process.env.AUTH_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
22
|
+
AUTH_LOGIN_MAX_ATTEMPTS: Number(process.env.AUTH_LOGIN_MAX_ATTEMPTS) || 10,
|
|
23
|
+
AUTH_RESET_MAX_ATTEMPTS: Number(process.env.AUTH_RESET_MAX_ATTEMPTS) || 5,
|
|
24
|
+
REFRESH_TOKEN_TTL_DAYS: Number(process.env.REFRESH_TOKEN_TTL_DAYS) || 60,
|
|
25
|
+
SWAGGER_UI_USER: process.env.SWAGGER_UI_USER || '',
|
|
26
|
+
SWAGGER_UI_PASSWORD: process.env.SWAGGER_UI_PASSWORD || '',
|
|
27
|
+
CORS_OPTIONS: {
|
|
28
|
+
origin: "*",
|
|
29
|
+
methods: ["GET", "POST", "PUT", "DELETE"] as ALLOWED_METHODS[]
|
|
30
|
+
}
|
|
19
31
|
}
|
|
20
32
|
export const API_VERSION = `/api/client/${DEFAULT_CONFIG.API_VERSION}`
|
|
21
33
|
export const HTTPS_SCHEMA = DEFAULT_CONFIG.HTTPS_SCHEMA
|
|
@@ -25,7 +37,8 @@ export const DB_NAME = database_name
|
|
|
25
37
|
export const AUTH_CONFIG = {
|
|
26
38
|
authCollection: auth_collection,
|
|
27
39
|
userCollection: collection_name,
|
|
28
|
-
resetPasswordCollection: '
|
|
40
|
+
resetPasswordCollection: 'reset_password_requests',
|
|
41
|
+
refreshTokensCollection: 'auth_refresh_tokens',
|
|
29
42
|
resetPasswordConfig: configuration['local-userpass']?.config,
|
|
30
43
|
user_id_field,
|
|
31
44
|
on_user_creation_function_name,
|
|
@@ -39,4 +52,4 @@ export const AUTH_CONFIG = {
|
|
|
39
52
|
export const S3_CONFIG = {
|
|
40
53
|
ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
|
|
41
54
|
SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY
|
|
42
|
-
}
|
|
55
|
+
}
|
package/src/fastify.d.ts
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
import { FastifyRequest as FastifyRequestType } from 'fastify'
|
|
1
|
+
import { FastifyRequest as FastifyRequestType, FastifyReply } from 'fastify'
|
|
2
2
|
|
|
3
3
|
type User = Record<string, unknown>
|
|
4
4
|
type UserData = Record<string, unknown>
|
|
5
|
+
type JwtUserData = UserData
|
|
6
|
+
|
|
7
|
+
type JwtAccessPayload = {
|
|
8
|
+
typ: 'access'
|
|
9
|
+
id: string
|
|
10
|
+
user_data: JwtUserData
|
|
11
|
+
data: JwtUserData
|
|
12
|
+
custom_data?: JwtUserData
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type JwtRefreshPayload = {
|
|
16
|
+
typ: 'refresh'
|
|
17
|
+
baas_id: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type JwtPayload = JwtAccessPayload | JwtRefreshPayload
|
|
21
|
+
|
|
22
|
+
type JwtAccessUser = JwtAccessPayload & {
|
|
23
|
+
sub: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type JwtRefreshUser = JwtRefreshPayload & {
|
|
27
|
+
sub: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type JwtUser = JwtAccessUser | JwtRefreshUser
|
|
5
31
|
|
|
6
32
|
declare module 'fastify' {
|
|
7
33
|
interface FastifyInstance {
|
|
@@ -9,20 +35,11 @@ declare module 'fastify' {
|
|
|
9
35
|
createAccessToken(user: User): string
|
|
10
36
|
createRefreshToken(user: User): string
|
|
11
37
|
}
|
|
38
|
+
}
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
typ: 'refresh'
|
|
18
|
-
sub: string
|
|
19
|
-
user_data: UserData
|
|
20
|
-
user: User
|
|
21
|
-
}
|
|
22
|
-
| {
|
|
23
|
-
typ: 'access'
|
|
24
|
-
user_data: UserData
|
|
25
|
-
id: string
|
|
26
|
-
}
|
|
40
|
+
declare module '@fastify/jwt' {
|
|
41
|
+
interface FastifyJWT {
|
|
42
|
+
payload: JwtPayload
|
|
43
|
+
user: JwtUser
|
|
27
44
|
}
|
|
28
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ObjectId } from 'bson'
|
|
2
|
-
import {
|
|
2
|
+
import { ChangeStream, Document } from 'mongodb';
|
|
3
|
+
import type { FastifyRequest } from 'fastify'
|
|
3
4
|
import { services } from '../../services'
|
|
4
5
|
import { StateManager } from '../../state'
|
|
5
6
|
import { GenerateContext } from '../../utils/context'
|
|
@@ -7,6 +8,32 @@ import { Base64Function, FunctionCallBase64Dto, FunctionCallDto } from './dtos'
|
|
|
7
8
|
import { FunctionController } from './interface'
|
|
8
9
|
import { executeQuery } from './utils'
|
|
9
10
|
|
|
11
|
+
const normalizeUser = (payload: Record<string, any> | undefined) => {
|
|
12
|
+
if (!payload) return undefined
|
|
13
|
+
const nestedUser =
|
|
14
|
+
payload.data ?? payload.user_data ?? payload.custom_data ?? payload
|
|
15
|
+
const flattened =
|
|
16
|
+
typeof nestedUser === 'object' && nestedUser !== null ? nestedUser : {}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...payload,
|
|
20
|
+
...flattened,
|
|
21
|
+
custom_data: payload.custom_data ?? flattened,
|
|
22
|
+
user_data: payload.user_data ?? flattened,
|
|
23
|
+
data: payload.data ?? flattened
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getRequestUser = (req: FastifyRequest) => {
|
|
28
|
+
const candidate = req.user as Record<string, any> | undefined
|
|
29
|
+
return normalizeUser(candidate)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const logFunctionCall = (method: string, user: Record<string, any> | undefined, args: unknown[]) => {
|
|
33
|
+
if (process.env.DEBUG_FUNCTIONS !== 'true') return
|
|
34
|
+
console.log('[functions-debug]', method, user ? { id: user.id, role: user.role, email: user.email } : 'no-user', args)
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
/**
|
|
11
38
|
* > Creates a pre handler for every query
|
|
12
39
|
* @param app -> the fastify instance
|
|
@@ -19,8 +46,13 @@ export const functionsController: FunctionController = async (
|
|
|
19
46
|
) => {
|
|
20
47
|
app.addHook('preHandler', app.jwtAuthentication)
|
|
21
48
|
|
|
49
|
+
const streams = {} as Record<string, ChangeStream<Document, Document>>
|
|
50
|
+
|
|
22
51
|
app.post<{ Body: FunctionCallDto }>('/call', async (req, res) => {
|
|
23
|
-
const
|
|
52
|
+
const user = getRequestUser(req)
|
|
53
|
+
if (!user || user.typ !== 'access') {
|
|
54
|
+
throw new Error('Access token required')
|
|
55
|
+
}
|
|
24
56
|
const { name: method, arguments: args } = req.body
|
|
25
57
|
|
|
26
58
|
if ('service' in req.body) {
|
|
@@ -29,16 +61,31 @@ export const functionsController: FunctionController = async (
|
|
|
29
61
|
if (!serviceFn) {
|
|
30
62
|
throw new Error(`Service "${req.body.service}" does not exist`)
|
|
31
63
|
}
|
|
32
|
-
const [{
|
|
64
|
+
const [{
|
|
65
|
+
database,
|
|
66
|
+
collection,
|
|
67
|
+
query,
|
|
68
|
+
filter,
|
|
69
|
+
update,
|
|
70
|
+
options,
|
|
71
|
+
returnNewDocument,
|
|
72
|
+
document,
|
|
73
|
+
documents,
|
|
74
|
+
pipeline = []
|
|
75
|
+
}] = args
|
|
33
76
|
|
|
34
77
|
const currentMethod = serviceFn(app, { rules, user })
|
|
35
78
|
.db(database)
|
|
36
79
|
.collection(collection)[method]
|
|
37
80
|
|
|
81
|
+
logFunctionCall(`service:${req.body.service}:${method}`, user, args)
|
|
38
82
|
const operatorsByType = await executeQuery({
|
|
39
83
|
currentMethod,
|
|
40
84
|
query,
|
|
85
|
+
filter,
|
|
41
86
|
update,
|
|
87
|
+
options,
|
|
88
|
+
returnNewDocument,
|
|
42
89
|
document,
|
|
43
90
|
documents,
|
|
44
91
|
pipeline,
|
|
@@ -57,6 +104,7 @@ export const functionsController: FunctionController = async (
|
|
|
57
104
|
throw new Error(`Function "${req.body.name}" is private`)
|
|
58
105
|
}
|
|
59
106
|
|
|
107
|
+
logFunctionCall(`function:${method}`, user, args)
|
|
60
108
|
const result = await GenerateContext({
|
|
61
109
|
args: req.body.arguments,
|
|
62
110
|
app,
|
|
@@ -72,7 +120,11 @@ export const functionsController: FunctionController = async (
|
|
|
72
120
|
app.get<{
|
|
73
121
|
Querystring: FunctionCallBase64Dto
|
|
74
122
|
}>('/call', async (req, res) => {
|
|
75
|
-
const { query
|
|
123
|
+
const { query } = req
|
|
124
|
+
const user = getRequestUser(req)
|
|
125
|
+
if (!user || user.typ !== 'access') {
|
|
126
|
+
throw new Error('Access token required')
|
|
127
|
+
}
|
|
76
128
|
const { baas_request, stitch_request } = query
|
|
77
129
|
|
|
78
130
|
const config: Base64Function = JSON.parse(
|
|
@@ -88,28 +140,43 @@ export const functionsController: FunctionController = async (
|
|
|
88
140
|
'Cache-Control': 'no-cache',
|
|
89
141
|
'Connection': 'keep-alive',
|
|
90
142
|
"access-control-allow-credentials": "true",
|
|
91
|
-
"access-control-allow-origin":
|
|
143
|
+
"access-control-allow-origin": '*',
|
|
92
144
|
"access-control-allow-headers": "X-Stitch-Location, X-Baas-Location, Location",
|
|
93
|
-
}
|
|
145
|
+
};
|
|
94
146
|
|
|
95
147
|
res.raw.writeHead(200, headers)
|
|
96
148
|
res.raw.flushHeaders();
|
|
97
149
|
|
|
98
|
-
const
|
|
150
|
+
const requestKey = baas_request || stitch_request
|
|
151
|
+
|
|
152
|
+
if (!requestKey) return
|
|
153
|
+
|
|
154
|
+
const changeStream = streams[requestKey]
|
|
155
|
+
|
|
156
|
+
if (changeStream) {
|
|
157
|
+
changeStream.on('change', (change) => {
|
|
158
|
+
res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
req.raw.on('close', () => {
|
|
162
|
+
console.log("change stream closed");
|
|
163
|
+
changeStream?.close?.();
|
|
164
|
+
delete streams[requestKey]
|
|
165
|
+
});
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
streams[requestKey] = await services['mongodb-atlas'](app, {
|
|
99
170
|
user,
|
|
100
171
|
rules
|
|
101
172
|
})
|
|
102
173
|
.db(database)
|
|
103
174
|
.collection(collection)
|
|
104
|
-
.watch([], { fullDocument: 'whenAvailable' })
|
|
175
|
+
.watch([], { fullDocument: 'whenAvailable' });
|
|
105
176
|
|
|
106
|
-
changeStream.on('change', (change) => {
|
|
107
|
-
res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
|
|
108
|
-
});
|
|
109
177
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
changeStream.close();
|
|
178
|
+
streams[requestKey].on('change', (change) => {
|
|
179
|
+
res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
|
|
113
180
|
});
|
|
114
181
|
})
|
|
115
182
|
}
|
|
@@ -23,8 +23,11 @@ export type FunctionCallBase64Dto = {
|
|
|
23
23
|
type ArgumentsData = Arguments<{
|
|
24
24
|
database: string
|
|
25
25
|
collection: string
|
|
26
|
+
filter?: Document
|
|
26
27
|
query: Parameters<GetOperatorsFunction>
|
|
27
28
|
update: Document
|
|
29
|
+
options?: Document
|
|
30
|
+
returnNewDocument?: boolean
|
|
28
31
|
document: Document
|
|
29
32
|
documents: Document[]
|
|
30
33
|
pipeline?: Document[]
|
|
@@ -24,6 +24,9 @@ export type ExecuteQueryParams = {
|
|
|
24
24
|
currentMethod: ReturnType<GetOperatorsFunction>[keyof ReturnType<GetOperatorsFunction>]
|
|
25
25
|
query: Parameters<GetOperatorsFunction>
|
|
26
26
|
update: Document
|
|
27
|
+
filter?: Document
|
|
28
|
+
options?: Document
|
|
29
|
+
returnNewDocument?: boolean
|
|
27
30
|
document: Document
|
|
28
31
|
documents: Document[]
|
|
29
32
|
pipeline: Document[]
|
|
@@ -44,29 +44,51 @@ export const executeQuery = async ({
|
|
|
44
44
|
currentMethod,
|
|
45
45
|
query,
|
|
46
46
|
update,
|
|
47
|
+
filter,
|
|
48
|
+
options,
|
|
49
|
+
returnNewDocument,
|
|
47
50
|
document,
|
|
48
51
|
documents,
|
|
49
52
|
pipeline,
|
|
50
53
|
isClient = false
|
|
51
54
|
}: ExecuteQueryParams) => {
|
|
55
|
+
const resolvedQuery =
|
|
56
|
+
typeof query !== 'undefined'
|
|
57
|
+
? query
|
|
58
|
+
: typeof filter !== 'undefined'
|
|
59
|
+
? filter
|
|
60
|
+
: {}
|
|
61
|
+
const resolvedUpdate = typeof update !== 'undefined' ? update : {}
|
|
62
|
+
const resolvedOptions =
|
|
63
|
+
typeof options !== 'undefined'
|
|
64
|
+
? options
|
|
65
|
+
: typeof returnNewDocument === 'boolean'
|
|
66
|
+
? { returnDocument: returnNewDocument ? 'after' : 'before' }
|
|
67
|
+
: undefined
|
|
52
68
|
return {
|
|
53
69
|
find: async () =>
|
|
54
70
|
await (currentMethod as ReturnType<GetOperatorsFunction>['find'])(
|
|
55
|
-
EJSON.deserialize(
|
|
71
|
+
EJSON.deserialize(resolvedQuery)
|
|
56
72
|
).toArray(),
|
|
57
73
|
findOne: () =>
|
|
58
74
|
(currentMethod as ReturnType<GetOperatorsFunction>['findOne'])(
|
|
59
|
-
EJSON.deserialize(
|
|
75
|
+
EJSON.deserialize(resolvedQuery)
|
|
60
76
|
),
|
|
61
77
|
deleteOne: () =>
|
|
62
78
|
(currentMethod as ReturnType<GetOperatorsFunction>['deleteOne'])(
|
|
63
|
-
EJSON.deserialize(
|
|
79
|
+
EJSON.deserialize(resolvedQuery)
|
|
64
80
|
),
|
|
65
81
|
insertOne: () =>
|
|
66
82
|
(currentMethod as ReturnType<GetOperatorsFunction>['insertOne'])(
|
|
67
83
|
EJSON.deserialize(document)
|
|
68
84
|
),
|
|
69
|
-
updateOne: () => currentMethod(EJSON.deserialize(
|
|
85
|
+
updateOne: () => currentMethod(EJSON.deserialize(resolvedQuery), EJSON.deserialize(resolvedUpdate)),
|
|
86
|
+
findOneAndUpdate: () =>
|
|
87
|
+
(currentMethod as ReturnType<GetOperatorsFunction>['findOneAndUpdate'])(
|
|
88
|
+
EJSON.deserialize(resolvedQuery),
|
|
89
|
+
EJSON.deserialize(resolvedUpdate),
|
|
90
|
+
resolvedOptions ? EJSON.deserialize(resolvedOptions) : undefined
|
|
91
|
+
),
|
|
70
92
|
aggregate: async () =>
|
|
71
93
|
(await (currentMethod as ReturnType<GetOperatorsFunction>['aggregate'])(
|
|
72
94
|
EJSON.deserialize(pipeline),
|
|
@@ -79,13 +101,12 @@ export const executeQuery = async ({
|
|
|
79
101
|
),
|
|
80
102
|
updateMany: () =>
|
|
81
103
|
(currentMethod as ReturnType<GetOperatorsFunction>['updateMany'])(
|
|
82
|
-
EJSON.deserialize(
|
|
83
|
-
EJSON.deserialize(
|
|
104
|
+
EJSON.deserialize(resolvedQuery),
|
|
105
|
+
EJSON.deserialize(resolvedUpdate)
|
|
84
106
|
),
|
|
85
107
|
deleteMany: () =>
|
|
86
108
|
(currentMethod as ReturnType<GetOperatorsFunction>['deleteMany'])(
|
|
87
|
-
EJSON.deserialize(
|
|
109
|
+
EJSON.deserialize(resolvedQuery)
|
|
88
110
|
)
|
|
89
111
|
}
|
|
90
112
|
}
|
|
91
|
-
|
|
@@ -5,11 +5,20 @@ import { Rules, RulesConfig } from './interface'
|
|
|
5
5
|
|
|
6
6
|
export const loadRules = async (rootDir = process.cwd()): Promise<Rules> => {
|
|
7
7
|
const rulesRoot = path.join(rootDir, 'data_sources', 'mongodb-atlas')
|
|
8
|
-
const
|
|
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
|
+
const files = recursivelyCollectFiles(rulesRoot)
|
|
9
18
|
const rulesFiles = files.filter((x) => (x as string).endsWith('rules.json'))
|
|
10
19
|
|
|
11
20
|
const rulesByCollection = rulesFiles.reduce((acc, rulesFile) => {
|
|
12
|
-
const filePath =
|
|
21
|
+
const filePath = rulesFile
|
|
13
22
|
const collectionRules = readJsonContent(filePath) as RulesConfig
|
|
14
23
|
acc[collectionRules.collection] = collectionRules
|
|
15
24
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AUTH_CONFIG, DB_NAME } from '../../constants'
|
|
1
2
|
import { services } from '../../services'
|
|
2
3
|
import { Function, Functions } from '../functions/interface'
|
|
3
4
|
import { ActivateTriggersParams } from './dtos'
|
|
@@ -17,7 +18,48 @@ export const activateTriggers = async ({
|
|
|
17
18
|
}: ActivateTriggersParams) => {
|
|
18
19
|
console.log('START ACTIVATION TRIGGERS')
|
|
19
20
|
try {
|
|
20
|
-
|
|
21
|
+
const triggersToActivate = [...triggersList]
|
|
22
|
+
if (AUTH_CONFIG.on_user_creation_function_name) {
|
|
23
|
+
const alreadyDeclared = triggersToActivate.some(
|
|
24
|
+
(trigger) =>
|
|
25
|
+
trigger.content.type === 'AUTHENTICATION' &&
|
|
26
|
+
trigger.content.event_processors?.FUNCTION?.config?.function_name ===
|
|
27
|
+
AUTH_CONFIG.on_user_creation_function_name
|
|
28
|
+
)
|
|
29
|
+
if (!alreadyDeclared) {
|
|
30
|
+
triggersToActivate.push({
|
|
31
|
+
fileName: '__auto_on_user_creation_trigger__.json',
|
|
32
|
+
content: {
|
|
33
|
+
name: 'onUserCreation',
|
|
34
|
+
type: 'AUTHENTICATION',
|
|
35
|
+
disabled: false,
|
|
36
|
+
config: {
|
|
37
|
+
collection: AUTH_CONFIG.authCollection ?? 'auth_users',
|
|
38
|
+
database: DB_NAME,
|
|
39
|
+
full_document: true,
|
|
40
|
+
full_document_before_change: false,
|
|
41
|
+
match: {},
|
|
42
|
+
operation_types: ['insert', 'update', 'replace'],
|
|
43
|
+
project: {},
|
|
44
|
+
service_name: 'mongodb-atlas',
|
|
45
|
+
skip_catchup_events: false,
|
|
46
|
+
tolerate_resume_errors: false,
|
|
47
|
+
unordered: false,
|
|
48
|
+
schedule: ''
|
|
49
|
+
},
|
|
50
|
+
event_processors: {
|
|
51
|
+
FUNCTION: {
|
|
52
|
+
config: {
|
|
53
|
+
function_name: AUTH_CONFIG.on_user_creation_function_name
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for await (const trigger of triggersToActivate) {
|
|
21
63
|
const { content } = trigger
|
|
22
64
|
const { type, config, event_processors } = content
|
|
23
65
|
|