@cocreate/authorize 1.15.0 → 1.16.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.
package/src/index.js CHANGED
@@ -1,369 +1,934 @@
1
1
  (function (root, factory) {
2
- if (typeof define === 'function' && define.amd) {
3
- define(["@cocreate/crud-client", "@cocreate/utils"], function (crud, { getValueFromObject, dotNotationToObject }) {
4
- return factory(true, crud, { getValueFromObject, dotNotationToObject })
5
- });
6
- } else if (typeof module === 'object' && module.exports) {
7
- const { getValueFromObject, dotNotationToObject } = require("@cocreate/utils");
8
- module.exports = class CoCreateAuthorize {
9
- constructor(crud) {
10
- return factory(false, crud, { getValueFromObject, dotNotationToObject });
11
- }
12
- }
13
- } else {
14
- root.returnExports = factory(true, root["@cocreate/crud-client"], root["@cocreate/utils"]);
15
- }
16
- }(typeof self !== 'undefined' ? self : this, function (isBrowser, crud, { getValueFromObject, dotNotationToObject }) {
17
-
18
- const organizations = {}
19
-
20
- if (isBrowser) {
21
- crud.listen('object.update', function (data) {
22
- updateAuthorization(data)
23
- });
24
-
25
- crud.listen('object.delete', function (data) {
26
- deleteAuthorization(data)
27
- });
28
- } else {
29
- process.on('crud-event', async (data) => {
30
- updateAuthorization(data)
31
- })
32
- }
33
-
34
- async function updateAuthorization(data) {
35
- const { array, object, organization_id } = data
36
-
37
- if (array === 'keys' && object) {
38
- let authorization = object[0]
39
- if (authorization && authorization.key && organizations[organization_id] && organizations[organization_id][authorization.key]) {
40
- let newAuthorization = await readAuthorization(authorization.key, data)
41
- organizations[organization_id][authorization.key] = newAuthorization
42
- }
43
- }
44
- }
45
-
46
- async function deleteAuthorization(data) {
47
- const { array, object, organization_id } = data
48
-
49
- if (array === 'keys' && object) {
50
- let authorization = object[0]
51
- if (authorization && authorization.key && organizations[organization_id] && organizations[organization_id][authorization.key]) {
52
- delete organizations[organization_id][authorization.key]
53
- }
54
- }
55
- }
56
-
57
- async function getAuthorization(key, data) {
58
- let organization_id = data.organization_id
59
- if (!organizations[organization_id])
60
- organizations[organization_id] = {}
61
-
62
- if (!organizations[organization_id][key])
63
- organizations[organization_id][key] = readAuthorization(key, data);
64
-
65
- organizations[organization_id][key] = await organizations[organization_id][key]
66
- return organizations[organization_id][key]
67
- }
68
-
69
- async function readAuthorization(key, data) {
70
- try {
71
- let organization_id = data.organization_id
72
- if (!organization_id)
73
- return { error: 'An organization_id is required' };
74
-
75
- let request = {
76
- method: 'object.read',
77
- host: data.host,
78
- database: organization_id,
79
- array: 'keys',
80
- organization_id,
81
- $filter: {
82
- query: {}
83
- }
84
- }
85
-
86
- if (key)
87
- request.$filter.query.key = key
88
- else
89
- request.$filter.query.default = true
90
-
91
-
92
- let authorization = await crud.send(request)
93
- if (authorization && authorization.object && authorization.object[0]) {
94
- authorization = authorization.object[0]
95
-
96
- if (!authorization.arrays) {
97
- authorization.arrays = {};
98
- }
99
-
100
- if (authorization && authorization.roles) {
101
- const role_ids = []
102
- authorization.roles.forEach((_id) => {
103
- if (_id)
104
- role_ids.push({ _id })
105
- })
106
-
107
- delete request.$filter
108
- delete request.isFilter
109
-
110
- request.object = role_ids
111
-
112
- let roles = await crud.send(request)
113
- roles = roles.object
114
-
115
- for (let role of roles) {
116
- authorization = dotNotationToObject(authorization, role)
117
- }
118
- }
119
- return authorization;
120
- } else
121
- return {}
122
- } catch (error) {
123
- console.log("authorization Error", error)
124
- return { error };
125
- }
126
-
127
- }
128
-
129
- async function check(data, user_id) {
130
- let authorization = false
131
- if (user_id) {
132
- authorization = await checkAuthorization({
133
- key: user_id,
134
- data
135
- })
136
- }
137
- if (!authorization || authorization.error) {
138
- authorization = await checkAuthorization({
139
- key: data.apikey,
140
- data
141
- })
142
- }
143
- return authorization;
144
- }
145
-
146
- async function checkAuthorization({ key, data }) {
147
- if (!data.organization_id)
148
- return { error: 'organization_id is required' };
149
- if (!data.method)
150
- return { error: 'method is required' };
151
-
152
- let authorized = await getAuthorization(key, data)
153
- if (!authorized || authorized.error)
154
- return authorized
155
- if (authorized.organization_id !== data.organization_id)
156
- return false;
157
- if (authorized.host && authorized.host.length) {
158
- if (!authorized.host || (!authorized.host.includes(data.host) && !authorized.host.includes("*")))
159
- return false;
160
- }
161
- if (authorized.admin === 'true' || authorized.admin === true)
162
- return true;
163
- if (!authorized.actions)
164
- return { error: "Authorization does not have any actions defined" };
165
-
166
- let status = await checkMethod(data, authorized.actions, data.method)
167
-
168
- // console.log(data.method, data.array, status)
169
-
170
- if (!status) {
171
- return false
172
- }
173
-
174
- return { authorized: data };
175
- }
176
-
177
- async function checkMethod(data, authorized, method) {
178
- if (authorized[method]) {
179
- return await checkMethodMatch(data, authorized[method], method)
180
- } else if (method.includes('.')) {
181
- let match = ''
182
- let splitMethod = method.split('.')
183
- for (let i = 0; i < splitMethod.length; i++) {
184
- if (!match)
185
- match = splitMethod[i]
186
- else
187
- match += '.' + splitMethod[i]
188
- if (authorized[match]) {
189
- return await checkMethodMatch(data, authorized[match], match)
190
- } else if (i === splitMethod.length - 1)
191
- return false
192
- }
193
- } else
194
- return false;
195
- }
196
-
197
- async function checkMethodMatch(data, authorized, match) {
198
- if (typeof authorized === 'boolean') {
199
- return authorized
200
- } else if (typeof authorized === 'string') {
201
- if (authorized === 'false')
202
- return false
203
- else if (authorized === 'true')
204
- return true
205
- else if (authorized === match)
206
- return true // check string for match or mutate data
207
- else
208
- return true // check string for match or mutate data
209
- } else if (typeof authorized === 'number') {
210
- return !!authorized
211
- } else {
212
- let status = false
213
- let newmatch = data.method.replace(match + '.', '')
214
-
215
- if (Array.isArray(authorized)) {
216
- for (let i = 0; i < authorized.length; i++) {
217
- status = await checkMethodMatch(data, authorized[i], newmatch)
218
- }
219
- } else if (typeof authorized === 'object') {
220
- for (const key of Object.keys(authorized)) {
221
- if (key.includes('$')) {
222
- if (['$storage', '$database', '$array', '$index'].includes(key)) {
223
- let opStatus = await checkMethodOperators(data, key, authorized[key])
224
- if (opStatus === true || opStatus === false)
225
- status = opStatus
226
- } else {
227
- let isFilter = applyFilter(data, authorized[key], key)
228
- console.log('isFIlter', isFilter)
229
- }
230
- }
231
- }
232
- }
233
- if (newmatch) {
234
- if (!status && authorized[newmatch]) {
235
- status = await checkMethodMatch(data, authorized[newmatch], newmatch)
236
- }
237
- if (!status && authorized['*']) {
238
- status = await checkMethodMatch(data, authorized['*'], newmatch)
239
- }
240
- }
241
-
242
- return status
243
- }
244
- }
245
-
246
- async function checkMethodOperators(data, key, authorization) {
247
- try {
248
- // Adjust authorization if it's based on a dynamic user ID from sockets
249
- if (authorization === '$user_id' && data.socket) {
250
- authorization = data.socket.user_id || data.user_id;
251
- }
252
-
253
- if (key.startsWith('$')) {
254
- let type = key.substring(1);
255
-
256
- if (!data[type]) {
257
- return undefined;
258
- }
259
-
260
- if (typeof data[type] === 'string') {
261
- return checkAuthorizationData(data, authorization, data[type])
262
- } else if (Array.isArray(data[type])) {
263
- if (!data[type].length)
264
- return undefined
265
- // ToDo: Current stratergy checks if all items match else false will be returned
266
- let allAuthorized = true;
267
- for (let i = 0; i < data[type].length; i++) {
268
- const itemData = typeof data[type][i] === 'object' ? data[type][i].name : data[type][i];
269
- const authResult = checkAuthorizationData(data, authorization, itemData);
270
- if (authResult === false) {
271
- return false; // Return false as soon as one item is unauthorized
272
- } else if (authResult === undefined) {
273
- allAuthorized = undefined
274
- }
275
- }
276
- return allAuthorized;
277
- } else if (typeof data[type] === 'object') {
278
- return checkAuthorizationData(data, authorization, data[type].name)
279
- }
280
- }
281
- return undefined;
282
- } catch (e) {
283
- console.log(e);
284
- return undefined;
285
- }
286
- }
287
-
288
- function checkAuthorizationData(data, authorization, key) {
289
- if (typeof authorization === 'string') {
290
- if (key === authorization)
291
- return true
292
- else
293
- return undefined
294
- } else if (Array.isArray(authorization)) {
295
- if (authorization.includes(key))
296
- return true
297
- else
298
- return undefined
299
- } else if (typeof authorization === 'object') {
300
- if (typeof authorization[key] === 'object') {
301
- for (const queryKey of Object.keys(authorization[key])) {
302
- return applyFilter(data, authorization[key], queryKey)
303
- }
304
- } else if (authorization[key] === false || authorization[key] === 'false')
305
- return false
306
- else if (authorization[key])
307
- return true
308
- else
309
- return undefined
310
- }
311
- }
312
-
313
- function applyFilter(data, authorization, authorizationKey) {
314
- let keyParts = authorizationKey.split('.');
315
- let operator = keyParts.pop();
316
- let key = keyParts.join('.');
317
- if (['$eq', '$ne', '$lt', '$lte', '$gt', '$gte', '$in', '$nin', '$or', '$and', '$not', '$nor', '$exists', '$type', '$mod', '$regex', '$text', '$where', '$all', '$elemMatch', '$size'].includes(operator)) {
318
- if (!data.$filter)
319
- data.$filter = { query: {} }
320
- else if (!data.$filter.query)
321
- data.$filter.query = {}
322
-
323
- if (authorization[authorizationKey] === '$user_id' && data.socket) {
324
- authorization[authorizationKey] = data.socket.user_id || data.user_id;
325
- }
326
-
327
- data.$filter.query[key] = { [operator]: authorization[authorizationKey] }
328
-
329
- return true
330
- }
331
- }
332
-
333
- async function checkFilter(authorized, data, apikey, unauthorize) {
334
- if (data.object.$filter && data.object.$filter.query) {
335
- let key
336
- if (data.object.$filter.type == 'object')
337
- key = '_id'
338
- else if (data.object.$filter.type == 'array')
339
- key = 'name'
340
- if (key) {
341
- for (let value of authorized[apikey]) {
342
- if (value[key])
343
- value = value[key]
344
- if (!data.object.$filter.query.$or)
345
- data.object.$filter.query.$or = []
346
- if (unauthorize)
347
- data.object.$filter.query.$or.push({ [key]: { $ne: value } })
348
- else
349
- data.object.$filter.query.$or.push({ [key]: value })
350
- }
351
- if (!unauthorize)
352
- return true
353
- }
354
- }
355
- }
356
-
357
- function deleteKey(data, path) {
358
- if (!data || !path) return
359
- if (path.includes('._id'))
360
- path = path.replace('._id', '');
361
- data = dotNotationToObject({ [path]: undefined }, data)
362
- return data
363
- }
364
-
365
- return {
366
- check
367
- }
368
-
369
- }));
2
+ if (typeof define === "function" && define.amd) {
3
+ define(["@cocreate/crud-client", "@cocreate/utils"], function (
4
+ crud,
5
+ { getValueFromObject, dotNotationToObject }
6
+ ) {
7
+ return factory(true, crud, {
8
+ getValueFromObject,
9
+ dotNotationToObject
10
+ });
11
+ });
12
+ } else if (typeof module === "object" && module.exports) {
13
+ const {
14
+ getValueFromObject,
15
+ dotNotationToObject
16
+ } = require("@cocreate/utils");
17
+ module.exports = class CoCreateAuthorize {
18
+ constructor(crud) {
19
+ return factory(false, crud, {
20
+ getValueFromObject,
21
+ dotNotationToObject
22
+ });
23
+ }
24
+ };
25
+ } else {
26
+ root.returnExports = factory(
27
+ true,
28
+ root["@cocreate/crud-client"],
29
+ root["@cocreate/utils"]
30
+ );
31
+ }
32
+ })(
33
+ typeof self !== "undefined" ? self : this,
34
+ function (isBrowser, crud, { getValueFromObject, dotNotationToObject }) {
35
+ const organizations = {};
36
+
37
+ if (isBrowser) {
38
+ crud.listen("object.update", function (data) {
39
+ updateAuthorization(data);
40
+ });
41
+
42
+ crud.listen("object.delete", function (data) {
43
+ deleteAuthorization(data);
44
+ });
45
+ } else {
46
+ process.on("crud-event", async (data) => {
47
+ updateAuthorization(data);
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Updates the cached authorization for a specific key within an organization.
53
+ *
54
+ * This function checks if the `array` is `"keys"` and if an authorization object is provided.
55
+ * If the conditions are met, it fetches the latest authorization details using the `readAuthorization`
56
+ * function and updates the cache for the given `organization_id` and `key`.
57
+ *
58
+ * @param {object} data - The data object containing:
59
+ * - `array`: The type of collection being updated (e.g., "keys").
60
+ * - `object`: The object array containing the authorization details.
61
+ * - `organization_id`: The ID of the organization the authorization belongs to.
62
+ * @returns {Promise<void>} Resolves when the update is complete.
63
+ */
64
+ async function updateAuthorization(data) {
65
+ const { array, object, organization_id } = data;
66
+
67
+ // Ensure we're working with "keys" and an authorization object is provided
68
+ if (array === "keys" && object) {
69
+ let authorization = object[0];
70
+
71
+ // Check if the authorization key exists in the cache
72
+ if (
73
+ authorization &&
74
+ authorization.key &&
75
+ organizations[organization_id] &&
76
+ organizations[organization_id][authorization.key]
77
+ ) {
78
+ // Fetch the latest authorization details
79
+ let newAuthorization = await readAuthorization(
80
+ authorization.key,
81
+ data
82
+ );
83
+
84
+ // Update the cache with the new authorization details
85
+ organizations[organization_id][authorization.key] =
86
+ newAuthorization;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Deletes the cached authorization for a specific key within an organization.
93
+ *
94
+ * This function checks if the `array` is `"keys"` and if an authorization object is provided.
95
+ * If the conditions are met, it removes the cached authorization for the given
96
+ * `organization_id` and `key` from the `organizations` cache.
97
+ *
98
+ * @param {object} data - The data object containing:
99
+ * - `array`: The type of collection being deleted (e.g., "keys").
100
+ * - `object`: The object array containing the authorization details.
101
+ * - `organization_id`: The ID of the organization the authorization belongs to.
102
+ * @returns {Promise<void>} Resolves when the deletion is complete.
103
+ */
104
+ async function deleteAuthorization(data) {
105
+ const { array, object, organization_id } = data;
106
+
107
+ // Ensure we're working with "keys" and an authorization object is provided
108
+ if (array === "keys" && object) {
109
+ let authorization = object[0];
110
+
111
+ // Check if the authorization key exists in the cache
112
+ if (
113
+ authorization &&
114
+ authorization.key &&
115
+ organizations[organization_id] &&
116
+ organizations[organization_id][authorization.key]
117
+ ) {
118
+ // Remove the cached authorization for the given key
119
+ delete organizations[organization_id][authorization.key];
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Retrieves the cached authorization for a specific key within an organization.
126
+ *
127
+ * If the authorization is not cached, this function fetches the authorization
128
+ * details using the `readAuthorization` function and stores it in the cache.
129
+ *
130
+ * @param {string} key - The authorization key (e.g., user ID or API key).
131
+ * @param {object} data - The data object containing:
132
+ * - `organization_id`: The ID of the organization the authorization belongs to.
133
+ * @returns {Promise<object>} A promise that resolves to the authorization details.
134
+ */
135
+ async function getAuthorization(key, data) {
136
+ let organization_id = data.organization_id;
137
+
138
+ // Initialize the cache for the organization if it doesn't exist
139
+ if (!organizations[organization_id])
140
+ organizations[organization_id] = {};
141
+
142
+ // If the authorization for the key is not cached, fetch and cache it
143
+ if (!organizations[organization_id][key])
144
+ organizations[organization_id][key] = readAuthorization(
145
+ key,
146
+ data
147
+ );
148
+
149
+ // Await the authorization (handles the promise stored during the initial fetch)
150
+ organizations[organization_id][key] = await organizations[
151
+ organization_id
152
+ ][key];
153
+
154
+ return organizations[organization_id][key];
155
+ }
156
+
157
+ /**
158
+ * Reads authorization details for a given key (user ID or API key) and organization.
159
+ *
160
+ * This function constructs a request to retrieve authorization details from the database.
161
+ * If the `key` is provided, it queries for the specific authorization key.
162
+ * If no `key` is provided, it queries for the default authorization settings.
163
+ *
164
+ * Additionally, it resolves roles associated with the authorization and merges role permissions
165
+ * into the final authorization object.
166
+ *
167
+ * @param {string} key - The authorization key (e.g., user ID or API key).
168
+ * @param {object} data - The data object containing request details, such as the `organization_id` and `host`.
169
+ * @returns {Promise<object>} A promise that resolves to the authorization object or an error message.
170
+ */
171
+ async function readAuthorization(key, data) {
172
+ try {
173
+ // Extract and validate the organization_id
174
+ let organization_id = data.organization_id;
175
+ if (!organization_id)
176
+ return { error: "An organization_id is required" };
177
+
178
+ // Construct the initial request for authorization data
179
+ let request = {
180
+ method: "object.read", // CRUD method to read the object
181
+ host: data.host, // Host associated with the request
182
+ database: organization_id, // Database scoped to the organization
183
+ array: "keys", // Collection or array to query
184
+ organization_id, // Organization context for the request
185
+ $filter: {
186
+ query: {} // Query filters to apply
187
+ }
188
+ };
189
+
190
+ // Add query filters based on the key or default authorization
191
+ if (key) request.$filter.query.key = key;
192
+ else request.$filter.query.default = true;
193
+
194
+ // Send the request to the CRUD layer
195
+ let authorization = await crud.send(request);
196
+
197
+ // If authorization data exists, process the roles and permissions
198
+ if (
199
+ authorization &&
200
+ authorization.object &&
201
+ authorization.object[0]
202
+ ) {
203
+ authorization = authorization.object[0];
204
+
205
+ // Ensure the 'arrays' field exists to prevent undefined references
206
+ if (!authorization.arrays) {
207
+ authorization.arrays = {};
208
+ }
209
+
210
+ // Process roles associated with the authorization
211
+ if (authorization && authorization.roles) {
212
+ const role_ids = [];
213
+ authorization.roles.forEach((_id) => {
214
+ if (_id) role_ids.push({ _id });
215
+ });
216
+
217
+ // Prepare a new request to fetch role details
218
+ delete request.$filter;
219
+ delete request.isFilter;
220
+
221
+ request.object = role_ids;
222
+
223
+ // Retrieve and merge role permissions into the authorization object
224
+ let roles = await crud.send(request);
225
+ roles = roles.object;
226
+
227
+ for (let role of roles) {
228
+ authorization = dotNotationToObject(
229
+ authorization,
230
+ role
231
+ );
232
+ }
233
+ }
234
+ return authorization; // Return the final authorization object
235
+ } else {
236
+ // Return an empty object if no authorization is found
237
+ return {};
238
+ }
239
+ } catch (error) {
240
+ // Handle and log any errors during the process
241
+ console.log("authorization Error", error);
242
+ return { error };
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Validates the authorization of a user or an API key to perform an action.
248
+ *
249
+ * The function first checks authorization using the provided `user_id`.
250
+ * If that fails or returns an error, it checks the authorization using the `apikey`
251
+ * present in the `data` object.
252
+ *
253
+ * @param {object} data - The data object containing request details, such as the `apikey` and `organization_id`.
254
+ * @param {string} user_id - The user ID for authorization validation.
255
+ * @returns {Promise<object|boolean>} A promise that resolves to the authorization result:
256
+ * - `false` if unauthorized.
257
+ * - An object with authorization details if authorized.
258
+ */
259
+ async function check(data, user_id) {
260
+ let authorization = false;
261
+
262
+ // Attempt to check authorization with the user ID if provided
263
+ if (user_id) {
264
+ authorization = await checkAuthorization({
265
+ key: user_id,
266
+ data
267
+ });
268
+ }
269
+
270
+ // Fallback to checking authorization with the API key if user ID check fails
271
+ if (!authorization || authorization.error) {
272
+ authorization = await checkAuthorization({
273
+ key: data.apikey,
274
+ data
275
+ });
276
+ }
277
+
278
+ return authorization;
279
+ }
280
+
281
+ /**
282
+ * Checks whether a given key (user ID or API key) is authorized to perform the specified action.
283
+ *
284
+ * The function validates the presence of required fields in the `data` object (`organization_id`, `method`),
285
+ * fetches authorization details, and performs checks such as:
286
+ * - Organization ID matching.
287
+ * - Host-level restrictions.
288
+ * - Admin privileges.
289
+ * - Defined actions and their permissions.
290
+ *
291
+ * @param {object} params - The parameters for the function.
292
+ * @param {string} params.key - The key used for authorization (user ID or API key).
293
+ * @param {object} params.data - The data object containing details like `organization_id`, `host`, and `method`.
294
+ * @returns {Promise<object|boolean>} A promise that resolves to:
295
+ * - An object with an error message if validation fails.
296
+ * - `false` if authorization checks fail.
297
+ * - `true` or an object with sanitized `data` if authorized.
298
+ */
299
+ async function checkAuthorization({ key, data }) {
300
+ // Validate required fields
301
+ if (!data.organization_id)
302
+ return { error: "organization_id is required" };
303
+ if (!data.method) return { error: "method is required" };
304
+
305
+ // Fetch authorization details using the provided key
306
+ let authorized = await getAuthorization(key, data);
307
+
308
+ // If no authorization details or an error is returned, propagate the error or deny access
309
+ if (!authorized || authorized.error) return authorized;
310
+
311
+ // Ensure the organization ID matches
312
+ if (authorized.organization_id !== data.organization_id)
313
+ return false;
314
+
315
+ // Validate host restrictions if specified
316
+ if (authorized.host && authorized.host.length) {
317
+ if (
318
+ !authorized.host ||
319
+ (!authorized.host.includes(data.host) &&
320
+ !authorized.host.includes("*"))
321
+ )
322
+ return false;
323
+ }
324
+
325
+ // Allow access if the user has admin privileges
326
+ if (authorized.admin === "true" || authorized.admin === true)
327
+ return true;
328
+
329
+ // Ensure actions are defined for the authorization
330
+ if (!authorized.actions)
331
+ return {
332
+ error: "Authorization does not have any actions defined"
333
+ };
334
+
335
+ // Check if the requested method is allowed under the defined actions
336
+ let status = await checkMethod(
337
+ data,
338
+ authorized.actions,
339
+ data.method
340
+ );
341
+
342
+ // If the method check fails, deny access
343
+ if (!status) {
344
+ return false;
345
+ }
346
+
347
+ // Return the sanitized and authorized data if all checks pass
348
+ return { authorized: data };
349
+ }
350
+
351
+ /**
352
+ * Validates whether a method is permitted based on the authorization rules.
353
+ *
354
+ * This function checks for an exact match of the `method` in the `authorized` actions.
355
+ * If no exact match is found, it checks progressively for partial matches using dot notation (e.g., "read.user").
356
+ *
357
+ * @param {object} data - The data object containing request details.
358
+ * @param {object} authorized - The authorization rules object mapping methods to permissions.
359
+ * @param {string} method - The method to validate (e.g., "read.user").
360
+ * @returns {Promise<boolean>} A promise that resolves to `true` if the method is authorized, `false` otherwise.
361
+ */
362
+ async function checkMethod(data, authorized, method) {
363
+ // Check for an exact match of the method in the authorization rules
364
+ if (authorized[method]) {
365
+ return await checkMethodMatch(data, authorized[method], method);
366
+ }
367
+ // Check for partial matches using dot notation (e.g., "read.user")
368
+ else if (method.includes(".")) {
369
+ let match = "";
370
+ let splitMethod = method.split(".");
371
+
372
+ for (let i = 0; i < splitMethod.length; i++) {
373
+ if (!match) match = splitMethod[i];
374
+ else match += "." + splitMethod[i];
375
+
376
+ // If a partial match is found, validate it
377
+ if (authorized[match]) {
378
+ return await checkMethodMatch(
379
+ data,
380
+ authorized[match],
381
+ match
382
+ );
383
+ }
384
+ // If the last segment has no match, deny access
385
+ else if (i === splitMethod.length - 1) return false;
386
+ }
387
+ }
388
+ // Deny access if no match is found
389
+ else return false;
390
+ }
391
+
392
+ /**
393
+ * Checks whether a given method matches the authorized permissions and
394
+ * optionally sanitizes the provided data based on inclusion and exclusion rules.
395
+ *
396
+ * This function supports various types of authorization configurations, including:
397
+ * - Booleans: Directly allow (`true`) or deny (`false`).
398
+ * - Strings: Match the authorization string with the method or default to allow.
399
+ * - Numbers: Treat any non-zero number as `true` (authorized).
400
+ * - Arrays: Recursively check multiple authorization rules.
401
+ * - Objects: Evaluate nested authorization rules and apply inclusion/exclusion logic.
402
+ *
403
+ * The function also aggregates all inclusion and exclusion rules before sanitizing
404
+ * the data, ensuring consistent prioritization logic is applied holistically.
405
+ *
406
+ * @param {object} data - The data object containing the method and other fields to process.
407
+ * @param {boolean | string | number | object | Array} authorized - The authorization rules.
408
+ * @param {string} match - The method string to match against the authorization rules.
409
+ * @returns {Promise<boolean>} A promise that resolves to `true` if the method is authorized,
410
+ * `false` otherwise.
411
+ *
412
+ * @example
413
+ * // Example usage:
414
+ * const data = { method: "read", read: { field1: "value1", field2: "value2" } };
415
+ * const authorized = { read: [{ field1: true }, { field2: false }] };
416
+ * const match = "read";
417
+ *
418
+ * checkMethodMatch(data, authorized, match).then((isAuthorized) => {
419
+ * console.log(isAuthorized); // true
420
+ * console.log(data); // { method: "read", read: { field1: "value1" } }
421
+ * });
422
+ */
423
+ async function checkMethodMatch(data, authorized, match) {
424
+ if (typeof authorized === "boolean") {
425
+ return authorized;
426
+ } else if (typeof authorized === "string") {
427
+ if (authorized === "false") return false;
428
+ else if (authorized === "true") return true;
429
+ else if (authorized === match) return true;
430
+ else return true;
431
+ } else if (typeof authorized === "number") {
432
+ return !!authorized;
433
+ } else {
434
+ let status = false;
435
+ let newmatch = data.method.replace(match + ".", "");
436
+
437
+ // Aggregate raw keys
438
+ let rawKeys = [];
439
+
440
+ if (Array.isArray(authorized)) {
441
+ for (let i = 0; i < authorized.length; i++) {
442
+ status = await checkMethodMatch(
443
+ data,
444
+ authorized[i],
445
+ newmatch
446
+ );
447
+ }
448
+ } else if (typeof authorized === "object") {
449
+ for (const key of Object.keys(authorized)) {
450
+ if (key.includes("$")) {
451
+ if (
452
+ [
453
+ "$storage",
454
+ "$database",
455
+ "$array",
456
+ "$index"
457
+ ].includes(key)
458
+ ) {
459
+ let opStatus = await checkMethodOperators(
460
+ data,
461
+ key,
462
+ authorized[key]
463
+ );
464
+ if (opStatus === true || opStatus === false)
465
+ status = opStatus;
466
+ } else if (key === "$keys") {
467
+ let type = data.method.split(".")[0];
468
+ if (data[type]) {
469
+ rawKeys.push(authorized[key]); // Collect raw authorization keys
470
+ }
471
+ } else {
472
+ let isFilter = applyFilter(
473
+ data,
474
+ authorized[key],
475
+ key
476
+ );
477
+ console.log("isFilter", isFilter);
478
+ }
479
+ }
480
+ }
481
+ }
482
+
483
+ // Parse all raw keys together to get inclusion/exclusion
484
+ let { inclusion, exclusion } = parsePermissions(rawKeys);
485
+
486
+ // Apply sanitization if there are inclusion or exclusion rules
487
+ if (inclusion || exclusion) {
488
+ let type = data.method.split(".")[0]; // Extract type from method
489
+ if (data[type]) {
490
+ data[type] = sanitizeData(
491
+ data[type],
492
+ inclusion,
493
+ exclusion
494
+ );
495
+ status = true;
496
+ }
497
+ }
498
+
499
+ if (newmatch) {
500
+ if (!status && authorized[newmatch]) {
501
+ status = await checkMethodMatch(
502
+ data,
503
+ authorized[newmatch],
504
+ newmatch
505
+ );
506
+ }
507
+ if (!status && authorized["*"]) {
508
+ status = await checkMethodMatch(
509
+ data,
510
+ authorized["*"],
511
+ newmatch
512
+ );
513
+ }
514
+ }
515
+
516
+ return status;
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Checks method operators to validate authorization based on dynamic data types and keys.
522
+ *
523
+ * @param {Object} data - The input data object containing various properties.
524
+ * @param {string} key - A string key to determine the type of operation,
525
+ * starting with `$` for dynamic type-based checks.
526
+ * @param {string} authorization - The authorization identifier, which could be a static value
527
+ * or a dynamic reference like `$user_id`.
528
+ *
529
+ * @returns {boolean|undefined} - Returns `true` if authorized, `false` if unauthorized,
530
+ * or `undefined` if no validation applies.
531
+ *
532
+ * This function:
533
+ * - Dynamically resolves `authorization` when `$user_id` is used and `data.socket` contains user information.
534
+ * - Handles dynamic key-based operations using a `$` prefix (e.g., `$type`).
535
+ * - Validates authorization for strings, arrays, and objects in `data[type]`:
536
+ * - For strings: Directly validates using `checkAuthorizationData`.
537
+ * - For arrays: Validates each item. Returns `false` if any item fails validation.
538
+ * If all items are valid, returns `true`. Returns `undefined` if the array is empty.
539
+ * - For objects: Validates based on the `name` property of the object.
540
+ *
541
+ * Example Usage:
542
+ * ```
543
+ * const data = {
544
+ * socket: { user_id: "123" },
545
+ * roles: ["admin", "editor"]
546
+ * };
547
+ * const result = checkMethodOperators(data, "$roles", "$user_id");
548
+ * ```
549
+ */
550
+ async function checkMethodOperators(data, key, authorization) {
551
+ try {
552
+ // Adjust authorization dynamically if `$user_id` is provided and user info exists in the socket
553
+ if (authorization === "$user_id" && data.socket) {
554
+ authorization = data.socket.user_id || data.user_id;
555
+ }
556
+
557
+ // Check if the key starts with `$`, indicating a dynamic type-based operation
558
+ if (key.startsWith("$")) {
559
+ let type = key.substring(1); // Extract the type by removing `$`
560
+
561
+ // If the specified type does not exist in data, return undefined
562
+ if (!data[type]) {
563
+ return undefined;
564
+ }
565
+
566
+ // Handle cases where the type is a string
567
+ if (typeof data[type] === "string") {
568
+ return checkAuthorizationData(
569
+ data,
570
+ authorization,
571
+ data[type]
572
+ );
573
+ }
574
+ // Handle cases where the type is an array
575
+ else if (Array.isArray(data[type])) {
576
+ if (!data[type].length) return undefined; // Return undefined for empty arrays
577
+
578
+ // ToDo: Current strategy ensures all items must match; adjust as needed
579
+ let allAuthorized = true;
580
+ for (let i = 0; i < data[type].length; i++) {
581
+ const itemData =
582
+ typeof data[type][i] === "object"
583
+ ? data[type][i].name // Extract name if the item is an object
584
+ : data[type][i];
585
+ const authResult = checkAuthorizationData(
586
+ data,
587
+ authorization,
588
+ itemData
589
+ );
590
+ if (authResult === false) {
591
+ return false; // Return false if any item fails validation
592
+ } else if (authResult === undefined) {
593
+ allAuthorized = undefined; // Mark as undefined if any validation is inconclusive
594
+ }
595
+ }
596
+ return allAuthorized;
597
+ }
598
+ // Handle cases where the type is an object
599
+ else if (typeof data[type] === "object") {
600
+ return checkAuthorizationData(
601
+ data,
602
+ authorization,
603
+ data[type].name
604
+ );
605
+ }
606
+ }
607
+
608
+ // Return undefined if no conditions match
609
+ return undefined;
610
+ } catch (e) {
611
+ // Log the error and return undefined in case of an exception
612
+ console.log(e);
613
+ return undefined;
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Checks if the provided key is authorized based on the authorization rules.
619
+ *
620
+ * @param {Object} data - The data object to be used in filtering.
621
+ * @param {string|Array|Object} authorization - The authorization rules, which can be a string, array, or object.
622
+ * @param {string} key - The key to be checked against the authorization rules.
623
+ * @returns {boolean|undefined} - Returns true if authorized, false if explicitly unauthorized, and undefined if no rule matches.
624
+ */
625
+ function checkAuthorizationData(data, authorization, key) {
626
+ // If the authorization is a string, check if it matches the key.
627
+ if (typeof authorization === "string") {
628
+ if (key === authorization)
629
+ return true; // Key matches the authorization string.
630
+ else return undefined; // No match, return undefined.
631
+ }
632
+ // If the authorization is an array, check if the key is included in the array.
633
+ else if (Array.isArray(authorization)) {
634
+ if (authorization.includes(key))
635
+ return true; // Key is included in the array.
636
+ else return undefined; // Key not found, return undefined.
637
+ }
638
+ // If the authorization is an object, handle various object-based rules.
639
+ else if (typeof authorization === "object") {
640
+ // If the authorization[key] is an object, apply filters recursively.
641
+ if (typeof authorization[key] === "object") {
642
+ for (const queryKey of Object.keys(authorization[key])) {
643
+ return applyFilter(data, authorization[key], queryKey); // Apply the filter logic.
644
+ }
645
+ }
646
+ // If authorization[key] is explicitly false or "false", return false.
647
+ else if (
648
+ authorization[key] === false ||
649
+ authorization[key] === "false"
650
+ )
651
+ return false;
652
+ // If authorization[key] exists and is truthy, return true.
653
+ else if (authorization[key]) return true;
654
+ // If none of the above conditions match, return undefined.
655
+ else return undefined;
656
+ }
657
+ }
658
+
659
+ function applyFilter(data, authorization, authorizationKey) {
660
+ let keyParts = authorizationKey.split(".");
661
+ let operator = keyParts.pop();
662
+ let key = keyParts.join(".");
663
+ if (
664
+ [
665
+ "$eq",
666
+ "$ne",
667
+ "$lt",
668
+ "$lte",
669
+ "$gt",
670
+ "$gte",
671
+ "$in",
672
+ "$nin",
673
+ "$or",
674
+ "$and",
675
+ "$not",
676
+ "$nor",
677
+ "$exists",
678
+ "$type",
679
+ "$mod",
680
+ "$regex",
681
+ "$text",
682
+ "$where",
683
+ "$all",
684
+ "$elemMatch",
685
+ "$size"
686
+ ].includes(operator)
687
+ ) {
688
+ if (!data.$filter) data.$filter = { query: {} };
689
+ else if (!data.$filter.query) data.$filter.query = {};
690
+
691
+ if (
692
+ authorization[authorizationKey] === "$user_id" &&
693
+ data.socket
694
+ ) {
695
+ authorization[authorizationKey] =
696
+ data.socket.user_id || data.user_id;
697
+ }
698
+
699
+ if (typeof authorization === "string")
700
+ data.$filter.query[key] = {
701
+ [operator]: authorization
702
+ };
703
+ else
704
+ data.$filter.query[key] = {
705
+ [operator]: authorization[authorizationKey]
706
+ };
707
+
708
+ return true;
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Parses permissions into inclusion and exclusion arrays.
714
+ *
715
+ * @param {string | object | Array<string | object>} authorization - The authorization to parse.
716
+ * @returns {object} An object containing 'inclusion' and 'exclusion' arrays.
717
+ */
718
+ function parsePermissions(authorization) {
719
+ // Initialize local arrays for inclusion and exclusion
720
+ const inclusion = [];
721
+ const exclusion = [];
722
+
723
+ // Helper function to process a single permission entry
724
+ function processPermission(entry) {
725
+ if (typeof entry === "string") {
726
+ // Treat string as inclusion
727
+ inclusion.push(entry);
728
+ } else if (typeof entry === "object" && entry !== null) {
729
+ for (const [key, value] of Object.entries(entry)) {
730
+ if (value === true) {
731
+ inclusion.push(key);
732
+ } else if (value === false) {
733
+ exclusion.push(key);
734
+ }
735
+ // Ignore entries where value is neither true nor false
736
+ }
737
+ }
738
+ // Ignore entries that are neither string nor object
739
+ }
740
+
741
+ // Normalize the input into an array of permission entries
742
+ let permissionEntries = [];
743
+
744
+ if (Array.isArray(authorization)) {
745
+ permissionEntries = authorization;
746
+ } else if (
747
+ typeof authorization === "string" ||
748
+ (typeof authorization === "object" && authorization !== null)
749
+ ) {
750
+ permissionEntries = [authorization];
751
+ }
752
+ // If authorization is undefined or null, treat as empty
753
+
754
+ // Process each permission entry
755
+ permissionEntries.forEach(processPermission);
756
+
757
+ // Apply prioritization logic
758
+ if (inclusion.length > 0) {
759
+ // If any inclusion exists, disregard exclusions
760
+ return { inclusion, exclusion: null };
761
+ } else if (exclusion.length > 0) {
762
+ // If only exclusions exist, disregard inclusions
763
+ return { inclusion: null, exclusion };
764
+ } else {
765
+ // If neither exists, set both to null
766
+ return { inclusion: null, exclusion: null };
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Sanitizes data based on parsed permissions.
772
+ *
773
+ * @param {object | Array} data - The data object or array to sanitize.
774
+ * @param {Array} inclusion - The parsed inclusion array.
775
+ * @param {Array} exclusion - The parsed exclusion array.
776
+ * @returns {object | Array} The sanitized data object or array.
777
+ */
778
+ function sanitizeData(data, inclusion, exclusion) {
779
+ // If both inclusion and exclusion are null, return the data as is
780
+ if (inclusion === null && exclusion === null) {
781
+ return data;
782
+ }
783
+
784
+ if (Array.isArray(data)) {
785
+ return data.map((item) =>
786
+ sanitizeData(item, inclusion, exclusion)
787
+ );
788
+ } else if (typeof data === "object" && data !== null) {
789
+ let sanitized = {};
790
+
791
+ if (inclusion) {
792
+ // Include only specified fields (supports nested fields)
793
+ for (let i = 0; i < inclusion.length; i++) {
794
+ let inclusionData = generateDotNotation(
795
+ data,
796
+ inclusion[i]
797
+ );
798
+ sanitized = { ...sanitized, ...inclusionData };
799
+ }
800
+ return dotNotationToObject(sanitized);
801
+ } else if (exclusion) {
802
+ for (let i = 0; i < exclusion.length; i++) {
803
+ let exclusionData = generateDotNotation(
804
+ data,
805
+ exclusion[i],
806
+ true
807
+ );
808
+ sanitized = { ...sanitized, ...exclusionData };
809
+ }
810
+ return dotNotationToObject(sanitized, data);
811
+ }
812
+ }
813
+
814
+ return data;
815
+ }
816
+
817
+ /**
818
+ * Generates an object with dot notation keys from the given data and path.
819
+ *
820
+ * @param {Object} data - The source data object to extract values from.
821
+ * @param {string} path - A dot-separated string representing the path to access data.
822
+ * Supports array notation with `[]` (e.g., `users[].name`).
823
+ * @param {boolean} isEclusion - If true, the result will exclude the specified path by setting its value to `undefined`.
824
+ * If false, the result will include the value at the specified path.
825
+ *
826
+ * @returns {Object} - An object with dot notation keys representing the path in the data.
827
+ * Example: `{ "user.name": "John" }`
828
+ *
829
+ * @throws {Error} - Throws an error if the `path` parameter is not a string.
830
+ *
831
+ * The function supports:
832
+ * - Nested object traversal based on the `path`.
833
+ * - Array indexing when the path includes `[]` to handle elements dynamically.
834
+ *
835
+ * Key Details:
836
+ * - The `processKeys` function handles recursive traversal of the data structure.
837
+ * - If `isEclusion` is true, it marks the path with `undefined` to represent exclusion.
838
+ * - If the path points to an array (e.g., `users[].name`), it iterates through array elements,
839
+ * appending index-based keys (e.g., `users[0].name`, `users[1].name`).
840
+ *
841
+ * Example Usage:
842
+ * ```
843
+ * const data = {
844
+ * users: [
845
+ * { name: "John", age: 30 },
846
+ * { name: "Jane", age: 25 }
847
+ * ]
848
+ * };
849
+ *
850
+ * const result = generateDotNotation(data, "users[].name", false);
851
+ * // Output: { "users[0].name": "John", "users[1].name": "Jane" }
852
+ * ```
853
+ */
854
+ function generateDotNotation(data, path, isEclusion) {
855
+ if (typeof path !== "string")
856
+ throw new Error("Path must be a string");
857
+
858
+ let result = {};
859
+
860
+ function processKeys(dataItem, keys, prefix = "", result) {
861
+ if (dataItem === undefined) return;
862
+
863
+ if (!keys.length) {
864
+ if (isEclusion) {
865
+ result[prefix] = undefined;
866
+ } else {
867
+ result[prefix] = dataItem;
868
+ }
869
+ return;
870
+ }
871
+
872
+ let key = keys.shift();
873
+ let isArray = key.endsWith("[]");
874
+ if (isArray) key = key.slice(0, -2); // Remove [] from key
875
+
876
+ // Construct prefix
877
+ prefix = prefix ? `${prefix}.${key}` : key;
878
+
879
+ const currentData = getValueFromObject(dataItem, key); // Using your existing function
880
+
881
+ if (isArray && Array.isArray(currentData)) {
882
+ currentData.forEach((item, index) => {
883
+ const newPrefix = `${prefix}[${index}]`;
884
+ processKeys(item, [...keys], newPrefix, result);
885
+ });
886
+ } else if (!isArray && currentData !== undefined) {
887
+ processKeys(currentData, keys, prefix, result);
888
+ }
889
+ }
890
+
891
+ if (data && path.trim()) {
892
+ const keys = path.split(".");
893
+ processKeys(data, keys, "", result);
894
+ }
895
+
896
+ return result;
897
+ }
898
+
899
+ async function checkFilter(authorized, data, apikey, unauthorize) {
900
+ if (data.object.$filter && data.object.$filter.query) {
901
+ let key;
902
+ if (data.object.$filter.type == "object") key = "_id";
903
+ else if (data.object.$filter.type == "array") key = "name";
904
+ if (key) {
905
+ for (let value of authorized[apikey]) {
906
+ if (value[key]) value = value[key];
907
+ if (!data.object.$filter.query.$or)
908
+ data.object.$filter.query.$or = [];
909
+ if (unauthorize)
910
+ data.object.$filter.query.$or.push({
911
+ [key]: { $ne: value }
912
+ });
913
+ else
914
+ data.object.$filter.query.$or.push({
915
+ [key]: value
916
+ });
917
+ }
918
+ if (!unauthorize) return true;
919
+ }
920
+ }
921
+ }
922
+
923
+ function deleteKey(data, path) {
924
+ if (!data || !path) return;
925
+ if (path.includes("._id")) path = path.replace("._id", "");
926
+ data = dotNotationToObject({ [path]: undefined }, data);
927
+ return data;
928
+ }
929
+
930
+ return {
931
+ check
932
+ };
933
+ }
934
+ );