@lenne.tech/nest-server 11.1.10 → 11.1.12
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/dist/core/common/helpers/scim.helper.d.ts +1 -0
- package/dist/core/common/helpers/scim.helper.js +164 -0
- package/dist/core/common/helpers/scim.helper.js.map +1 -0
- package/dist/core/common/interfaces/scim-array-filter-node.interface.d.ts +6 -0
- package/dist/core/common/interfaces/scim-array-filter-node.interface.js +3 -0
- package/dist/core/common/interfaces/scim-array-filter-node.interface.js.map +1 -0
- package/dist/core/common/interfaces/scim-condition-node.interface.d.ts +7 -0
- package/dist/core/common/interfaces/scim-condition-node.interface.js +3 -0
- package/dist/core/common/interfaces/scim-condition-node.interface.js.map +1 -0
- package/dist/core/common/interfaces/scim-logical-node.interface.d.ts +7 -0
- package/dist/core/common/interfaces/scim-logical-node.interface.js +3 -0
- package/dist/core/common/interfaces/scim-logical-node.interface.js.map +1 -0
- package/dist/core/common/types/scim-comparator.type.d.ts +1 -0
- package/dist/core/common/types/scim-comparator.type.js +3 -0
- package/dist/core/common/types/scim-comparator.type.js.map +1 -0
- package/dist/core/common/types/scim-logical-operator.type.d.ts +1 -0
- package/dist/core/common/types/scim-logical-operator.type.js +3 -0
- package/dist/core/common/types/scim-logical-operator.type.js.map +1 -0
- package/dist/core/common/types/scim-node.type.d.ts +4 -0
- package/dist/core/common/types/scim-node.type.js +3 -0
- package/dist/core/common/types/scim-node.type.js.map +1 -0
- package/dist/core/modules/auth/services/core-auth.service.js +6 -1
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/core/common/helpers/scim.helper.ts +254 -0
- package/src/core/common/interfaces/scim-array-filter-node.interface.ts +8 -0
- package/src/core/common/interfaces/scim-condition-node.interface.ts +9 -0
- package/src/core/common/interfaces/scim-logical-node.interface.ts +9 -0
- package/src/core/common/types/scim-comparator.type.ts +2 -0
- package/src/core/common/types/scim-logical-operator.type.ts +1 -0
- package/src/core/common/types/scim-node.type.ts +6 -0
- package/src/core/modules/auth/services/core-auth.service.ts +6 -1
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.12",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"bcrypt": "5.1.1",
|
|
86
86
|
"class-transformer": "0.5.1",
|
|
87
87
|
"class-validator": "0.14.2",
|
|
88
|
-
"compression": "1.8.
|
|
88
|
+
"compression": "1.8.1",
|
|
89
89
|
"cookie-parser": "1.4.7",
|
|
90
90
|
"dotenv": "16.5.0",
|
|
91
91
|
"ejs": "3.1.10",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"@typescript-eslint/eslint-plugin": "8.32.0",
|
|
136
136
|
"@typescript-eslint/parser": "8.32.0",
|
|
137
137
|
"coffeescript": "2.7.0",
|
|
138
|
-
"eslint": "9.
|
|
138
|
+
"eslint": "9.32.0",
|
|
139
139
|
"eslint-config-prettier": "10.1.5",
|
|
140
140
|
"eslint-plugin-unused-imports": "4.1.4",
|
|
141
141
|
"find-file-up": "2.0.1",
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Comparator } from '../types/scim-comparator.type';
|
|
2
|
+
import type { LogicalOperator } from '../types/scim-logical-operator.type';
|
|
3
|
+
|
|
4
|
+
import { ScimNode } from '../types/scim-node.type';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function scimToMongo(scim: string): any {
|
|
8
|
+
if (!scim) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
const tokens = tokenize(scim);
|
|
12
|
+
const ast = parseTokens(tokens);
|
|
13
|
+
return transformAstToMongo(ast);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Escapes Regex Chars */
|
|
17
|
+
function escapeRegex(input: string): string {
|
|
18
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Flattens consecutive logical operators of the same type to avoid nested structures */
|
|
22
|
+
function flattenLogicalOperator(node: ScimNode, targetOperator: LogicalOperator): ScimNode[] {
|
|
23
|
+
if (!('operator' in node)) {
|
|
24
|
+
return [node];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.operator !== targetOperator) {
|
|
28
|
+
return [node];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const leftConditions = flattenLogicalOperator(node.left, targetOperator);
|
|
32
|
+
const rightConditions = flattenLogicalOperator(node.right, targetOperator);
|
|
33
|
+
|
|
34
|
+
return [...leftConditions, ...rightConditions];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parses tokenized SCIM filter into an Abstract Syntax Tree (AST) */
|
|
38
|
+
function parseTokens(tokens: string[]): ScimNode {
|
|
39
|
+
let pos = 0;
|
|
40
|
+
|
|
41
|
+
/** Parses a full logical expression (e.g., A and B or C) */
|
|
42
|
+
function parseExpression(): ScimNode {
|
|
43
|
+
let left = parseTerm();
|
|
44
|
+
while (tokens[pos] && /^(and|or)$/i.test(tokens[pos])) {
|
|
45
|
+
const op: LogicalOperator = tokens[pos++].toLowerCase() as LogicalOperator;
|
|
46
|
+
const right = parseTerm();
|
|
47
|
+
left = { left, operator: op, right };
|
|
48
|
+
}
|
|
49
|
+
return left;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parses a single term: either a nested expression, array filter, or condition */
|
|
53
|
+
function parseTerm(): ScimNode {
|
|
54
|
+
if (tokens[pos] === '(') { // Start of a nested filter
|
|
55
|
+
pos++; // skip '('
|
|
56
|
+
const expr = parseExpression();
|
|
57
|
+
if (tokens[pos] !== ')') {
|
|
58
|
+
throw new Error(`Expected ')' at position ${pos}`);
|
|
59
|
+
}
|
|
60
|
+
pos++; // skip ')'
|
|
61
|
+
return expr;
|
|
62
|
+
}
|
|
63
|
+
if (tokens[pos + 1] === '[') { // Start of an array Filter
|
|
64
|
+
const path = tokens[pos++];
|
|
65
|
+
pos++; // skip '['
|
|
66
|
+
const expr = parseExpression();
|
|
67
|
+
if (tokens[pos] !== ']') {
|
|
68
|
+
throw new Error(`Expected ']' at position ${pos}`);
|
|
69
|
+
}
|
|
70
|
+
pos++; // skip ']'
|
|
71
|
+
return { expr, path, type: 'array' };
|
|
72
|
+
}
|
|
73
|
+
return parseCondition(); // If its neither a nested nor array filter its a simple "propertyKey eq Value"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Parses a basic SCIM condition (e.g., userName eq "Joe") */
|
|
77
|
+
function parseCondition(): ScimNode {
|
|
78
|
+
const attr = tokens[pos++]; // First token is the attribute
|
|
79
|
+
const op: Comparator = tokens[pos++].toLowerCase() as Comparator; // Second token is the operator
|
|
80
|
+
if (!['aco', 'co', 'eq', 'ew', 'ge', 'gt', 'le', 'lt', 'pr', 'sw'].includes(op)) {
|
|
81
|
+
throw new Error(`Unsupported comparator: ${op}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let value: any = null;
|
|
85
|
+
|
|
86
|
+
if (op !== 'pr') { // "Is Present" doesnt require a value
|
|
87
|
+
let rawValue = tokens[pos++]; // Third token is the value
|
|
88
|
+
if (!attr || !op || rawValue === undefined) {
|
|
89
|
+
throw new Error(`Invalid condition syntax at token ${pos}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle quoted strings
|
|
93
|
+
if (rawValue?.startsWith('"')) {
|
|
94
|
+
rawValue = rawValue.slice(1, -1);
|
|
95
|
+
// For quoted strings, keep as string (don't parse to number/boolean)
|
|
96
|
+
value = rawValue;
|
|
97
|
+
} else {
|
|
98
|
+
// For unquoted values, parse to appropriate type (number, boolean, or string)
|
|
99
|
+
value = parseValue(rawValue);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { attributePath: attr, comparator: op, type: 'condition', value };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parseExpression();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/** Converts string values to appropriate types (number, boolean, or string) */
|
|
111
|
+
function parseValue(value: string): any {
|
|
112
|
+
if (value === null || value === undefined) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if it's a number (integer or float)
|
|
117
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
118
|
+
const numValue = Number(value);
|
|
119
|
+
return isNaN(numValue) ? value : numValue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if it's a boolean
|
|
123
|
+
if (value.toLowerCase() === 'true') {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (value.toLowerCase() === 'false') {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Return as string for everything else
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Tokenizes a SCIM filter string into meaningful parts.
|
|
136
|
+
* e.g., 'userName eq "john"' → ['userName', 'eq', '"john"']
|
|
137
|
+
*/
|
|
138
|
+
function tokenize(input: string): string[] {
|
|
139
|
+
// Space out brackets, but not inside quoted strings
|
|
140
|
+
let result = '';
|
|
141
|
+
let insideQuotes = false;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < input.length; i++) {
|
|
144
|
+
const char = input[i];
|
|
145
|
+
|
|
146
|
+
if (char === '"' && (i === 0 || input[i - 1] !== '\\')) {
|
|
147
|
+
insideQuotes = !insideQuotes;
|
|
148
|
+
result += char;
|
|
149
|
+
} else if (!insideQuotes && /[()[\]]/.test(char)) {
|
|
150
|
+
result += ` ${char} `;
|
|
151
|
+
} else {
|
|
152
|
+
result += char;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
.replace(/\s+/g, ' ') // Normalise whitespaces
|
|
158
|
+
.trim()
|
|
159
|
+
.match(/\[|]|\(|\)|[a-zA-Z0-9_.]+|"(?:[^"\\]|\\.)*"/g) || []; // Match tokens: brackets, identifiers, quoted strings
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Converts the parsed SCIM AST to an equivalent MongoDB query object */
|
|
163
|
+
function transformAstToMongo(node: ScimNode): any {
|
|
164
|
+
if (!node) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if ('operator' in node) {
|
|
169
|
+
const operator = node.operator;
|
|
170
|
+
const conditions = flattenLogicalOperator(node, operator);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
[`$${operator}`]: conditions.map(transformAstToMongo),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (node.type === 'array') {
|
|
178
|
+
return {
|
|
179
|
+
[node.path]: {
|
|
180
|
+
$elemMatch: transformAstToMongo(node.expr),
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { attributePath, comparator, value } = node;
|
|
186
|
+
switch (comparator) {
|
|
187
|
+
case 'aco': // ARRAY-CONTAINS (Not case sensitive)
|
|
188
|
+
return { [attributePath]: value };
|
|
189
|
+
case 'co': // CONTAINS
|
|
190
|
+
return { [attributePath]: { $options: 'i', $regex: escapeRegex(value) } };
|
|
191
|
+
case 'eq': // EQUALS
|
|
192
|
+
return { [attributePath]: { $eq: value } };
|
|
193
|
+
case 'ew': // ENDSWITH
|
|
194
|
+
return { [attributePath]: { $options: 'i', $regex: `${escapeRegex(value)}$` } };
|
|
195
|
+
case 'ge': // GREATER THAN OR EQUAL
|
|
196
|
+
return { [attributePath]: { $gte: value } };
|
|
197
|
+
case 'gt': // GREATER THAN
|
|
198
|
+
return { [attributePath]: { $gt: value } };
|
|
199
|
+
case 'le': // LESS THAN OR EQUAL
|
|
200
|
+
return { [attributePath]: { $lte: value } };
|
|
201
|
+
case 'lt': // LESS THAN
|
|
202
|
+
return { [attributePath]: { $lt: value } };
|
|
203
|
+
case 'pr': // PRESENT (exists)
|
|
204
|
+
return { [attributePath]: { $exists: true } };
|
|
205
|
+
case 'sw': // STARTSWITH
|
|
206
|
+
return { [attributePath]: { $options: 'i', $regex: `^${escapeRegex(value)}` } };
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`Unsupported comparator: ${comparator}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/*
|
|
213
|
+
================ EXAMPLES ================
|
|
214
|
+
|
|
215
|
+
Simple condition:
|
|
216
|
+
SCIM: 'userName eq "Joe"'
|
|
217
|
+
→ Tokens: ['userName', 'eq', '"Joe"']
|
|
218
|
+
→ AST: { attributePath: 'userName', comparator: 'eq', value: 'Joe' }
|
|
219
|
+
→ Mongo: { userName: { $eq: 'Joe' } }
|
|
220
|
+
|
|
221
|
+
Logical combination:
|
|
222
|
+
SCIM: 'userName eq "Joe" and drinksCoffee eq true'
|
|
223
|
+
→ Tokens: ['userName', 'eq', '"Joe"', 'and', 'drinksCoffee', 'eq', 'true']
|
|
224
|
+
→ AST:
|
|
225
|
+
{
|
|
226
|
+
operator: 'and',
|
|
227
|
+
left: { attributePath: 'userName', comparator: 'eq', value: 'Joe' },
|
|
228
|
+
right: { attributePath: 'drinksCoffee', comparator: 'eq', value: 'true' }
|
|
229
|
+
}
|
|
230
|
+
→ Mongo:
|
|
231
|
+
{
|
|
232
|
+
$and: [
|
|
233
|
+
{ userName: { $eq: 'Joe' } },
|
|
234
|
+
{ drinksCoffee: { $eq: 'true' } }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
Array filter:
|
|
239
|
+
SCIM: 'emails[type eq "work"]'
|
|
240
|
+
→ Tokens: ['emails', '[', 'type', 'eq', '"work"', ']']
|
|
241
|
+
→ AST:
|
|
242
|
+
{
|
|
243
|
+
type: 'array',
|
|
244
|
+
path: 'emails',
|
|
245
|
+
expr: { attributePath: 'type', comparator: 'eq', value: 'work' }
|
|
246
|
+
}
|
|
247
|
+
→ Mongo:
|
|
248
|
+
{
|
|
249
|
+
emails: {
|
|
250
|
+
$elemMatch: { type: { $eq: 'work' } }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LogicalOperator } from '../types/scim-logical-operator.type';
|
|
2
|
+
import { ScimNode } from '../types/scim-node.type';
|
|
3
|
+
|
|
4
|
+
/** Represents a logical operator node (e.g., X and Y, A or B) */
|
|
5
|
+
export interface LogicalNode {
|
|
6
|
+
left: ScimNode;
|
|
7
|
+
operator: LogicalOperator;
|
|
8
|
+
right: ScimNode;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type LogicalOperator = 'and' | 'or';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ArrayFilterNode } from '../interfaces/scim-array-filter-node.interface';
|
|
2
|
+
import { ConditionNode } from '../interfaces/scim-condition-node.interface';
|
|
3
|
+
import { LogicalNode } from '../interfaces/scim-logical-node.interface';
|
|
4
|
+
|
|
5
|
+
/** Union type representing any valid SCIM node */
|
|
6
|
+
export type ScimNode = ArrayFilterNode | ConditionNode | LogicalNode;
|
|
@@ -289,7 +289,12 @@ export class CoreAuthService {
|
|
|
289
289
|
if (!deviceId) {
|
|
290
290
|
deviceId = payload.deviceId;
|
|
291
291
|
}
|
|
292
|
-
user.refreshTokens[deviceId] = {
|
|
292
|
+
user.refreshTokens[deviceId] = {
|
|
293
|
+
...data,
|
|
294
|
+
deviceDescription: payload.deviceDescription || data.deviceDescription,
|
|
295
|
+
deviceId,
|
|
296
|
+
tokenId: payload.tokenId,
|
|
297
|
+
};
|
|
293
298
|
user.tempTokens[deviceId] = { createdAt: new Date().getTime(), deviceId, tokenId: payload.tokenId };
|
|
294
299
|
await this.userService.update(
|
|
295
300
|
getStringIds(user),
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from './core/common/helpers/filter.helper';
|
|
|
36
36
|
export * from './core/common/helpers/graphql.helper';
|
|
37
37
|
export * from './core/common/helpers/input.helper';
|
|
38
38
|
export * from './core/common/helpers/model.helper';
|
|
39
|
+
export * from './core/common/helpers/scim.helper';
|
|
39
40
|
export * from './core/common/helpers/service.helper';
|
|
40
41
|
export * from './core/common/helpers/table.helper';
|
|
41
42
|
export * from './core/common/inputs/combined-filter.input';
|