@airoom/nextmin-node 1.4.5 → 2.0.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/README.md +48 -5
- package/dist/api/apiRouter.d.ts +2 -0
- package/dist/api/apiRouter.js +68 -19
- package/dist/api/router/mountCrudRoutes.js +209 -221
- package/dist/api/router/mountFindRoutes.js +2 -49
- package/dist/api/router/mountSearchRoutes.js +10 -52
- package/dist/api/router/mountSearchRoutes_extended.js +7 -48
- package/dist/api/router/setupAuthRoutes.js +6 -2
- package/dist/api/router/utils.js +20 -7
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/database/DatabaseAdapter.d.ts +7 -0
- package/dist/database/NMAdapter.d.ts +41 -0
- package/dist/database/NMAdapter.js +979 -0
- package/dist/database/QueryEngine.d.ts +14 -0
- package/dist/database/QueryEngine.js +215 -0
- package/dist/database/utils.d.ts +2 -0
- package/dist/database/utils.js +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -5
- package/dist/models/BaseModel.d.ts +16 -0
- package/dist/models/BaseModel.js +32 -4
- package/dist/policy/authorize.js +118 -43
- package/dist/schemas/Users.json +66 -30
- package/dist/services/RealtimeService.d.ts +20 -0
- package/dist/services/RealtimeService.js +93 -0
- package/dist/services/SchemaService.d.ts +3 -0
- package/dist/services/SchemaService.js +9 -5
- package/dist/utils/DefaultDataInitializer.js +10 -2
- package/dist/utils/Events.d.ts +34 -0
- package/dist/utils/Events.js +55 -0
- package/dist/utils/Logger.js +12 -10
- package/dist/utils/QueryCache.d.ts +16 -0
- package/dist/utils/QueryCache.js +106 -0
- package/dist/utils/SchemaLoader.d.ts +7 -2
- package/dist/utils/SchemaLoader.js +58 -18
- package/package.json +19 -4
- package/dist/database/InMemoryAdapter.d.ts +0 -15
- package/dist/database/InMemoryAdapter.js +0 -71
- package/dist/database/MongoAdapter.d.ts +0 -52
- package/dist/database/MongoAdapter.js +0 -410
package/dist/policy/authorize.js
CHANGED
|
@@ -38,6 +38,23 @@ function computeSensitiveMask(schemaPolicy) {
|
|
|
38
38
|
return negs;
|
|
39
39
|
}
|
|
40
40
|
/* ----------------- general helpers ----------------- */
|
|
41
|
+
function getPolicySection(base, ...path) {
|
|
42
|
+
if (!base)
|
|
43
|
+
return undefined;
|
|
44
|
+
let curr = base;
|
|
45
|
+
for (let i = 0; i < path.length; i++) {
|
|
46
|
+
const part = path[i];
|
|
47
|
+
// try exact key (e.g. "roles.admin")
|
|
48
|
+
const flatKey = path.slice(i).join('.');
|
|
49
|
+
if (curr[flatKey] !== undefined)
|
|
50
|
+
return curr[flatKey];
|
|
51
|
+
// try nested
|
|
52
|
+
curr = curr[part];
|
|
53
|
+
if (curr === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return curr;
|
|
57
|
+
}
|
|
41
58
|
function asBool(v) {
|
|
42
59
|
return typeof v === 'boolean' ? v : undefined;
|
|
43
60
|
}
|
|
@@ -61,6 +78,41 @@ function isOwnerDoc(schemaPolicy, ctx, doc) {
|
|
|
61
78
|
const candidate = typeof val === 'object' ? (val.id ?? val._id ?? String(val)) : String(val);
|
|
62
79
|
return String(candidate) === String(ctx.userId);
|
|
63
80
|
}
|
|
81
|
+
const OPERATOR_MAP = {
|
|
82
|
+
ne: '$ne',
|
|
83
|
+
gt: '$gt',
|
|
84
|
+
gte: '$gte',
|
|
85
|
+
lt: '$lt',
|
|
86
|
+
lte: '$lte',
|
|
87
|
+
in: '$in',
|
|
88
|
+
nin: '$nin',
|
|
89
|
+
regex: '$regex',
|
|
90
|
+
options: '$options',
|
|
91
|
+
and: '$and',
|
|
92
|
+
or: '$or',
|
|
93
|
+
nor: '$nor',
|
|
94
|
+
};
|
|
95
|
+
function interpolate(val, ctx) {
|
|
96
|
+
if (typeof val === 'string') {
|
|
97
|
+
if (val === '$CTX.userId')
|
|
98
|
+
return ctx.userId;
|
|
99
|
+
if (val === '$CTX.role')
|
|
100
|
+
return ctx.role;
|
|
101
|
+
return val;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(val)) {
|
|
104
|
+
return val.map((v) => interpolate(v, ctx));
|
|
105
|
+
}
|
|
106
|
+
if (val && typeof val === 'object' && val.constructor === Object) {
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [k, v] of Object.entries(val)) {
|
|
109
|
+
const targetKey = OPERATOR_MAP[k] || k;
|
|
110
|
+
out[targetKey] = interpolate(v, ctx);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
return val;
|
|
115
|
+
}
|
|
64
116
|
/* =================================================== */
|
|
65
117
|
/* =============== MAIN AUTHORIZATION ================= */
|
|
66
118
|
/* =================================================== */
|
|
@@ -71,6 +123,15 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
71
123
|
return { ...EMPTY_DECISION, allow: false };
|
|
72
124
|
const access = schemaPolicy?.access || {};
|
|
73
125
|
const baseReadMask = [].concat(access?.readMask || []);
|
|
126
|
+
// Auto-mask private attributes
|
|
127
|
+
for (const [name, attr] of Object.entries(schemaPolicy?.attributes || {})) {
|
|
128
|
+
const a = Array.isArray(attr) ? attr[0] : attr;
|
|
129
|
+
if (a && a.private === true) {
|
|
130
|
+
const mask = `-${name}`;
|
|
131
|
+
if (!baseReadMask.includes(mask))
|
|
132
|
+
baseReadMask.push(mask);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
74
135
|
const baseWriteDeny = [].concat(access?.writeDeny || []);
|
|
75
136
|
const restrictionsBase = access?.restrictions || {};
|
|
76
137
|
const createDefaultsBase = access?.createDefaults || {};
|
|
@@ -83,27 +144,47 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
83
144
|
const effectiveWriteDeny = bypass ? [] : writeDenyIn || [];
|
|
84
145
|
return { effectiveReadMask, effectiveWriteDeny };
|
|
85
146
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const allowedByDefault = action === 'read' ? true : !!ctx.isAuthenticated;
|
|
89
|
-
const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
|
|
147
|
+
const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
|
|
148
|
+
const finalizeFallback = (dec) => {
|
|
90
149
|
return {
|
|
150
|
+
...dec,
|
|
151
|
+
queryFilter: interpolate(dec.queryFilter, ctx),
|
|
152
|
+
createDefaults: interpolate(dec.createDefaults, ctx),
|
|
153
|
+
restrictions: interpolate(dec.restrictions, ctx),
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
// 1) Reachability check: Do we have ANY explicit rules for this action?
|
|
157
|
+
const hasReachabilityRule = !!(access?.public?.[action] !== undefined ||
|
|
158
|
+
access?.authenticated?.[action] !== undefined ||
|
|
159
|
+
(access?.roles &&
|
|
160
|
+
Object.values(access.roles).some((r) => r[action] !== undefined)));
|
|
161
|
+
// If no access block OR no reachability rule for this specific action → sensible defaults
|
|
162
|
+
if (!schemaPolicy?.access || !hasReachabilityRule) {
|
|
163
|
+
const allowedByDefault = action === 'read' ? true : !!ctx.isAuthenticated;
|
|
164
|
+
return finalizeFallback({
|
|
91
165
|
allow: allowedByDefault,
|
|
92
166
|
readMask: effectiveReadMask,
|
|
93
167
|
writeDeny: effectiveWriteDeny,
|
|
94
168
|
createDefaults: {},
|
|
95
169
|
restrictions: {},
|
|
96
170
|
queryFilter: action === 'read' ? {} : {},
|
|
97
|
-
exposePrivate: bypass,
|
|
171
|
+
exposePrivate: bypass,
|
|
98
172
|
sensitiveMask,
|
|
99
|
-
};
|
|
173
|
+
});
|
|
100
174
|
}
|
|
175
|
+
const finalize = (dec) => {
|
|
176
|
+
return {
|
|
177
|
+
...dec,
|
|
178
|
+
queryFilter: interpolate(dec.queryFilter, ctx),
|
|
179
|
+
createDefaults: interpolate(dec.createDefaults, ctx),
|
|
180
|
+
restrictions: interpolate(dec.restrictions, ctx),
|
|
181
|
+
};
|
|
182
|
+
};
|
|
101
183
|
// 2) PUBLIC
|
|
102
184
|
const pubRule = access?.public?.[action];
|
|
103
185
|
const pubBool = asBool(pubRule);
|
|
104
186
|
if (pubBool === true) {
|
|
105
|
-
|
|
106
|
-
return {
|
|
187
|
+
return finalize({
|
|
107
188
|
allow: true,
|
|
108
189
|
readMask: effectiveReadMask,
|
|
109
190
|
writeDeny: effectiveWriteDeny,
|
|
@@ -112,43 +193,41 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
112
193
|
queryFilter: queryFilterBase?.public || {},
|
|
113
194
|
exposePrivate: bypass, // if caller has a bypass role, they get private fields (still masked by sensitive)
|
|
114
195
|
sensitiveMask,
|
|
115
|
-
};
|
|
196
|
+
});
|
|
116
197
|
}
|
|
117
198
|
// if false → continue to roles/auth checks
|
|
118
199
|
// 3) ROLES
|
|
119
200
|
const roleName = (ctx.role || '').toLowerCase();
|
|
120
|
-
|
|
121
|
-
|
|
201
|
+
const roleRules = roleName ? getPolicySection(access, 'roles', roleName) : null;
|
|
202
|
+
if (roleRules) {
|
|
203
|
+
const rule = roleRules[action];
|
|
122
204
|
const rBool = asBool(rule);
|
|
123
205
|
if (rBool === true) {
|
|
124
|
-
|
|
125
|
-
return {
|
|
206
|
+
return finalize({
|
|
126
207
|
allow: true,
|
|
127
208
|
readMask: effectiveReadMask,
|
|
128
209
|
writeDeny: effectiveWriteDeny,
|
|
129
|
-
createDefaults: createDefaultsBase
|
|
130
|
-
restrictions: restrictionsBase
|
|
131
|
-
queryFilter: queryFilterBase
|
|
210
|
+
createDefaults: getPolicySection(createDefaultsBase, 'roles', roleName) || {},
|
|
211
|
+
restrictions: getPolicySection(restrictionsBase, 'roles', roleName) || {},
|
|
212
|
+
queryFilter: getPolicySection(queryFilterBase, 'roles', roleName) || {},
|
|
132
213
|
exposePrivate: bypass,
|
|
133
214
|
sensitiveMask,
|
|
134
|
-
};
|
|
215
|
+
});
|
|
135
216
|
}
|
|
136
217
|
if (rBool === false) {
|
|
137
|
-
|
|
138
|
-
return {
|
|
218
|
+
return finalize({
|
|
139
219
|
...EMPTY_DECISION,
|
|
140
220
|
readMask: effectiveReadMask,
|
|
141
221
|
writeDeny: effectiveWriteDeny,
|
|
142
222
|
exposePrivate: false,
|
|
143
223
|
sensitiveMask,
|
|
144
|
-
};
|
|
224
|
+
});
|
|
145
225
|
}
|
|
146
226
|
// role === 'owner'
|
|
147
227
|
if (rule === 'owner') {
|
|
148
|
-
const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
|
|
149
228
|
if (action === 'read') {
|
|
150
229
|
if (doc) {
|
|
151
|
-
return {
|
|
230
|
+
return finalize({
|
|
152
231
|
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
153
232
|
readMask: effectiveReadMask,
|
|
154
233
|
writeDeny: effectiveWriteDeny,
|
|
@@ -157,9 +236,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
157
236
|
queryFilter: {},
|
|
158
237
|
exposePrivate: bypass,
|
|
159
238
|
sensitiveMask,
|
|
160
|
-
};
|
|
239
|
+
});
|
|
161
240
|
}
|
|
162
|
-
return {
|
|
241
|
+
return finalize({
|
|
163
242
|
allow: true,
|
|
164
243
|
readMask: effectiveReadMask,
|
|
165
244
|
writeDeny: effectiveWriteDeny,
|
|
@@ -168,9 +247,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
168
247
|
queryFilter: ownerFilter(schemaPolicy, ctx),
|
|
169
248
|
exposePrivate: bypass,
|
|
170
249
|
sensitiveMask,
|
|
171
|
-
};
|
|
250
|
+
});
|
|
172
251
|
}
|
|
173
|
-
return {
|
|
252
|
+
return finalize({
|
|
174
253
|
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
175
254
|
readMask: effectiveReadMask,
|
|
176
255
|
writeDeny: effectiveWriteDeny,
|
|
@@ -179,15 +258,14 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
179
258
|
queryFilter: {},
|
|
180
259
|
exposePrivate: bypass,
|
|
181
260
|
sensitiveMask,
|
|
182
|
-
};
|
|
261
|
+
});
|
|
183
262
|
}
|
|
184
263
|
}
|
|
185
264
|
// 4) AUTHENTICATED
|
|
186
265
|
const authRule = access?.authenticated?.[action];
|
|
187
266
|
const aBool = asBool(authRule);
|
|
188
267
|
if (aBool === true && ctx.isAuthenticated) {
|
|
189
|
-
|
|
190
|
-
return {
|
|
268
|
+
return finalize({
|
|
191
269
|
allow: true,
|
|
192
270
|
readMask: effectiveReadMask,
|
|
193
271
|
writeDeny: effectiveWriteDeny,
|
|
@@ -196,23 +274,21 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
196
274
|
queryFilter: queryFilterBase?.authenticated || {},
|
|
197
275
|
exposePrivate: bypass,
|
|
198
276
|
sensitiveMask,
|
|
199
|
-
};
|
|
277
|
+
});
|
|
200
278
|
}
|
|
201
279
|
if (aBool === false && ctx.isAuthenticated) {
|
|
202
|
-
|
|
203
|
-
return {
|
|
280
|
+
return finalize({
|
|
204
281
|
...EMPTY_DECISION,
|
|
205
282
|
readMask: effectiveReadMask,
|
|
206
283
|
writeDeny: effectiveWriteDeny,
|
|
207
284
|
exposePrivate: false,
|
|
208
285
|
sensitiveMask,
|
|
209
|
-
};
|
|
286
|
+
});
|
|
210
287
|
}
|
|
211
288
|
if (authRule === 'owner' && ctx.isAuthenticated) {
|
|
212
|
-
const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
|
|
213
289
|
if (action === 'read') {
|
|
214
290
|
if (doc) {
|
|
215
|
-
return {
|
|
291
|
+
return finalize({
|
|
216
292
|
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
217
293
|
readMask: effectiveReadMask,
|
|
218
294
|
writeDeny: effectiveWriteDeny,
|
|
@@ -221,9 +297,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
221
297
|
queryFilter: {},
|
|
222
298
|
exposePrivate: bypass,
|
|
223
299
|
sensitiveMask,
|
|
224
|
-
};
|
|
300
|
+
});
|
|
225
301
|
}
|
|
226
|
-
return {
|
|
302
|
+
return finalize({
|
|
227
303
|
allow: true,
|
|
228
304
|
readMask: effectiveReadMask,
|
|
229
305
|
writeDeny: effectiveWriteDeny,
|
|
@@ -232,9 +308,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
232
308
|
queryFilter: ownerFilter(schemaPolicy, ctx),
|
|
233
309
|
exposePrivate: bypass,
|
|
234
310
|
sensitiveMask,
|
|
235
|
-
};
|
|
311
|
+
});
|
|
236
312
|
}
|
|
237
|
-
return {
|
|
313
|
+
return finalize({
|
|
238
314
|
allow: isOwnerDoc(schemaPolicy, ctx, doc),
|
|
239
315
|
readMask: effectiveReadMask,
|
|
240
316
|
writeDeny: effectiveReadMask, // (typo guard: will be ignored by callers; leave as is)
|
|
@@ -243,17 +319,16 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
243
319
|
queryFilter: {},
|
|
244
320
|
exposePrivate: bypass,
|
|
245
321
|
sensitiveMask,
|
|
246
|
-
};
|
|
322
|
+
});
|
|
247
323
|
}
|
|
248
324
|
// 5) default deny
|
|
249
|
-
|
|
250
|
-
return {
|
|
325
|
+
return finalize({
|
|
251
326
|
...EMPTY_DECISION,
|
|
252
327
|
readMask: effectiveReadMask,
|
|
253
328
|
writeDeny: effectiveWriteDeny,
|
|
254
329
|
exposePrivate: false,
|
|
255
330
|
sensitiveMask,
|
|
256
|
-
};
|
|
331
|
+
});
|
|
257
332
|
}
|
|
258
333
|
/* ===== Helpers consumed by the router ===== */
|
|
259
334
|
function applyReadMaskOne(doc, mask) {
|
package/dist/schemas/Users.json
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
"username": {
|
|
5
5
|
"type": "string",
|
|
6
6
|
"required": true,
|
|
7
|
-
"unique": true
|
|
7
|
+
"unique": true,
|
|
8
|
+
"safe": true
|
|
8
9
|
},
|
|
9
10
|
"email": {
|
|
10
11
|
"type": "string",
|
|
11
12
|
"required": true,
|
|
12
|
-
"unique": true
|
|
13
|
+
"unique": true,
|
|
14
|
+
"private": true,
|
|
15
|
+
"safe": true
|
|
13
16
|
},
|
|
14
17
|
"firstName": {
|
|
15
18
|
"type": "string",
|
|
@@ -22,9 +25,9 @@
|
|
|
22
25
|
"password": {
|
|
23
26
|
"type": "string",
|
|
24
27
|
"required": true,
|
|
25
|
-
"private": true
|
|
28
|
+
"private": true,
|
|
29
|
+
"safe": true
|
|
26
30
|
},
|
|
27
|
-
|
|
28
31
|
"profilePicture": [
|
|
29
32
|
{
|
|
30
33
|
"type": "string",
|
|
@@ -35,7 +38,6 @@
|
|
|
35
38
|
"required": true
|
|
36
39
|
}
|
|
37
40
|
],
|
|
38
|
-
|
|
39
41
|
"role": [
|
|
40
42
|
{
|
|
41
43
|
"type": "ObjectId",
|
|
@@ -45,28 +47,35 @@
|
|
|
45
47
|
"required": true
|
|
46
48
|
}
|
|
47
49
|
],
|
|
48
|
-
|
|
49
50
|
"status": {
|
|
50
51
|
"type": "string",
|
|
51
|
-
"enum": [
|
|
52
|
+
"enum": [
|
|
53
|
+
"pending",
|
|
54
|
+
"active",
|
|
55
|
+
"suspended"
|
|
56
|
+
],
|
|
52
57
|
"default": "pending",
|
|
53
|
-
"required": true
|
|
58
|
+
"required": true,
|
|
59
|
+
"safe": true
|
|
54
60
|
},
|
|
55
61
|
"type": {
|
|
56
62
|
"type": "string",
|
|
57
|
-
"enum": [
|
|
63
|
+
"enum": [
|
|
64
|
+
"system",
|
|
65
|
+
"default",
|
|
66
|
+
"user"
|
|
67
|
+
],
|
|
58
68
|
"default": "user",
|
|
59
|
-
"required": true
|
|
69
|
+
"required": true,
|
|
70
|
+
"safe": true
|
|
60
71
|
}
|
|
61
72
|
},
|
|
62
|
-
|
|
63
73
|
"allowedMethods": {
|
|
64
74
|
"create": true,
|
|
65
75
|
"read": true,
|
|
66
76
|
"update": true,
|
|
67
77
|
"delete": false
|
|
68
78
|
},
|
|
69
|
-
|
|
70
79
|
"access": {
|
|
71
80
|
"public": {
|
|
72
81
|
"create": true,
|
|
@@ -77,10 +86,9 @@
|
|
|
77
86
|
"authenticated": {
|
|
78
87
|
"create": false,
|
|
79
88
|
"read": "owner",
|
|
80
|
-
"update":
|
|
89
|
+
"update": "owner",
|
|
81
90
|
"delete": false
|
|
82
91
|
},
|
|
83
|
-
|
|
84
92
|
"roles": {
|
|
85
93
|
"admin": {
|
|
86
94
|
"create": true,
|
|
@@ -95,28 +103,56 @@
|
|
|
95
103
|
"delete": true
|
|
96
104
|
}
|
|
97
105
|
},
|
|
98
|
-
|
|
99
106
|
"queryFilter": {
|
|
100
|
-
"authenticated": {
|
|
107
|
+
"authenticated": {
|
|
108
|
+
"id": {
|
|
109
|
+
"ne": "$CTX.userId"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
101
112
|
"roles": {
|
|
102
|
-
"admin":
|
|
103
|
-
|
|
113
|
+
"admin": {
|
|
114
|
+
"id": {
|
|
115
|
+
"ne": "$CTX.userId"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"superadmin": {}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"conditions": {
|
|
122
|
+
"owner": {
|
|
123
|
+
"by": "id"
|
|
104
124
|
}
|
|
105
125
|
},
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"writeDeny": [
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
"readMask": [
|
|
127
|
+
"-password"
|
|
128
|
+
],
|
|
129
|
+
"writeDeny": [
|
|
130
|
+
"password"
|
|
131
|
+
],
|
|
132
|
+
"bypassPrivacy": {
|
|
133
|
+
"roles": [
|
|
134
|
+
"admin",
|
|
135
|
+
"superadmin"
|
|
136
|
+
]
|
|
137
|
+
},
|
|
112
138
|
"createDefaults": {
|
|
113
|
-
"public": {
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
"public": {
|
|
140
|
+
"role": "user",
|
|
141
|
+
"status": "pending"
|
|
142
|
+
},
|
|
143
|
+
"roles.admin": {
|
|
144
|
+
"status": "active"
|
|
145
|
+
},
|
|
146
|
+
"roles.superadmin": {
|
|
147
|
+
"status": "active"
|
|
148
|
+
}
|
|
116
149
|
},
|
|
117
|
-
|
|
118
150
|
"restrictions": {
|
|
119
|
-
"roles.admin.cannotAssign": {
|
|
151
|
+
"roles.admin.cannotAssign": {
|
|
152
|
+
"role": [
|
|
153
|
+
"superadmin"
|
|
154
|
+
]
|
|
155
|
+
}
|
|
120
156
|
}
|
|
121
157
|
}
|
|
122
|
-
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Server as HTTPServer } from 'http';
|
|
2
|
+
export declare const SOCKET_PATH = "/__nextmin__/realtime";
|
|
3
|
+
export declare const NAMESPACE = "/realtime";
|
|
4
|
+
/**
|
|
5
|
+
* RealtimeService handles real-time communication with clients using Socket.io.
|
|
6
|
+
* It broadcasts schema changes and CRUD events.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RealtimeService {
|
|
9
|
+
private opts;
|
|
10
|
+
private io;
|
|
11
|
+
private nsp;
|
|
12
|
+
constructor(server: HTTPServer, opts: {
|
|
13
|
+
getApiKey?: () => string | undefined;
|
|
14
|
+
corsOrigin?: string | any;
|
|
15
|
+
});
|
|
16
|
+
private setupAuth;
|
|
17
|
+
private setupListeners;
|
|
18
|
+
private forwardSystemEvents;
|
|
19
|
+
shutdown(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RealtimeService = exports.NAMESPACE = exports.SOCKET_PATH = void 0;
|
|
7
|
+
const socket_io_1 = require("socket.io");
|
|
8
|
+
const SchemaLoader_1 = require("../utils/SchemaLoader");
|
|
9
|
+
const Events_1 = require("../utils/Events");
|
|
10
|
+
const Logger_1 = __importDefault(require("../utils/Logger"));
|
|
11
|
+
exports.SOCKET_PATH = '/__nextmin__/realtime';
|
|
12
|
+
exports.NAMESPACE = '/realtime';
|
|
13
|
+
/**
|
|
14
|
+
* RealtimeService handles real-time communication with clients using Socket.io.
|
|
15
|
+
* It broadcasts schema changes and CRUD events.
|
|
16
|
+
*/
|
|
17
|
+
class RealtimeService {
|
|
18
|
+
constructor(server, opts) {
|
|
19
|
+
this.opts = opts;
|
|
20
|
+
this.io = new socket_io_1.Server(server, {
|
|
21
|
+
path: exports.SOCKET_PATH,
|
|
22
|
+
cors: { origin: opts.corsOrigin ?? '*' },
|
|
23
|
+
});
|
|
24
|
+
this.nsp = this.io.of(exports.NAMESPACE);
|
|
25
|
+
this.setupAuth();
|
|
26
|
+
this.setupListeners();
|
|
27
|
+
this.forwardSystemEvents();
|
|
28
|
+
Logger_1.default.info('RealtimeService', `Initialized at ${exports.SOCKET_PATH}`);
|
|
29
|
+
}
|
|
30
|
+
setupAuth() {
|
|
31
|
+
this.nsp.use((socket, next) => {
|
|
32
|
+
const provided = socket.handshake.auth?.apiKey;
|
|
33
|
+
const trusted = this.opts.getApiKey?.();
|
|
34
|
+
if (trusted && provided === trusted)
|
|
35
|
+
return next();
|
|
36
|
+
next(new Error('unauthorized'));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
setupListeners() {
|
|
40
|
+
this.nsp.on('connection', (socket) => {
|
|
41
|
+
Logger_1.default.info('RealtimeService', `Client connected: ${socket.id}`);
|
|
42
|
+
const loader = SchemaLoader_1.SchemaLoader.getInstance();
|
|
43
|
+
socket.emit('schemasData', loader.getPublicSchemaList(true));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
forwardSystemEvents() {
|
|
47
|
+
// 1. Forward Schema changes
|
|
48
|
+
const loader = SchemaLoader_1.SchemaLoader.getInstance();
|
|
49
|
+
if (typeof loader.on === 'function') {
|
|
50
|
+
loader.on('schemasChanged', () => {
|
|
51
|
+
this.nsp.emit('schemasUpdated', loader.getPublicSchemaList(true));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// 2. Forward CRUD events (Events from our central emitter)
|
|
55
|
+
Events_1.events.on(Events_1.Events.AFTER_CREATE, (payload) => {
|
|
56
|
+
const model = payload.modelName.toLowerCase();
|
|
57
|
+
const data = {
|
|
58
|
+
model: payload.modelName,
|
|
59
|
+
action: 'created',
|
|
60
|
+
id: payload.result.id,
|
|
61
|
+
data: payload.result
|
|
62
|
+
};
|
|
63
|
+
this.nsp.emit('doc:created', data);
|
|
64
|
+
this.nsp.emit(`${model}:created`, data);
|
|
65
|
+
});
|
|
66
|
+
Events_1.events.on(Events_1.Events.AFTER_UPDATE, (payload) => {
|
|
67
|
+
const model = payload.modelName.toLowerCase();
|
|
68
|
+
const data = {
|
|
69
|
+
model: payload.modelName,
|
|
70
|
+
action: 'updated',
|
|
71
|
+
id: payload.id,
|
|
72
|
+
data: payload.result
|
|
73
|
+
};
|
|
74
|
+
this.nsp.emit('doc:updated', data);
|
|
75
|
+
this.nsp.emit(`${model}:updated`, data);
|
|
76
|
+
});
|
|
77
|
+
Events_1.events.on(Events_1.Events.AFTER_DELETE, (payload) => {
|
|
78
|
+
const model = payload.modelName.toLowerCase();
|
|
79
|
+
const data = {
|
|
80
|
+
model: payload.modelName,
|
|
81
|
+
action: 'deleted',
|
|
82
|
+
id: payload.id
|
|
83
|
+
};
|
|
84
|
+
this.nsp.emit('doc:deleted', data);
|
|
85
|
+
this.nsp.emit(`${model}:deleted`, data);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
shutdown() {
|
|
89
|
+
this.io.close();
|
|
90
|
+
Logger_1.default.warn('RealtimeService', 'Stopped');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.RealtimeService = RealtimeService;
|
|
@@ -7,4 +7,7 @@ export interface SchemaServiceOptions {
|
|
|
7
7
|
/** Optional CORS origin override (defaults to "*") */
|
|
8
8
|
corsOrigin?: string | RegExp | (string | RegExp)[];
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated The SchemaService is deprecated. Use RealtimeService instead.
|
|
12
|
+
*/
|
|
10
13
|
export declare function startSchemaService(server: HTTPServer, opts?: SchemaServiceOptions): void;
|
|
@@ -13,7 +13,11 @@ exports.NAMESPACE = '/schema';
|
|
|
13
13
|
let started = false;
|
|
14
14
|
let io = null;
|
|
15
15
|
let nsp = null;
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated The SchemaService is deprecated. Use RealtimeService instead.
|
|
18
|
+
*/
|
|
16
19
|
function startSchemaService(server, opts = {}) {
|
|
20
|
+
Logger_1.default.warn('SchemaService', 'The SchemaService is deprecated. Use RealtimeService instead.');
|
|
17
21
|
if (started)
|
|
18
22
|
return;
|
|
19
23
|
started = true;
|
|
@@ -34,13 +38,13 @@ function startSchemaService(server, opts = {}) {
|
|
|
34
38
|
const loader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
|
|
35
39
|
nsp.on('connection', (socket) => {
|
|
36
40
|
Logger_1.default.info('SchemaService:', socket.id);
|
|
37
|
-
// Send
|
|
38
|
-
socket.emit('schemasData', loader.getPublicSchemaList());
|
|
41
|
+
// Send normalized (secure) snapshot on connect
|
|
42
|
+
socket.emit('schemasData', loader.getPublicSchemaList(true));
|
|
39
43
|
});
|
|
40
|
-
// Broadcast updates on hot-reload / schema changes
|
|
44
|
+
// Broadcast sanitized updates on hot-reload / schema changes
|
|
41
45
|
if (typeof loader.on === 'function') {
|
|
42
|
-
loader.on('schemasChanged', (
|
|
43
|
-
nsp?.emit('schemasUpdated',
|
|
46
|
+
loader.on('schemasChanged', () => {
|
|
47
|
+
nsp?.emit('schemasUpdated', loader.getPublicSchemaList(true));
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -235,8 +235,16 @@ class DefaultDataInitializer {
|
|
|
235
235
|
// Update only missing/empty fields (do not overwrite existing values)
|
|
236
236
|
const updates = {};
|
|
237
237
|
// apiKey
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
const envKey = process.env.NEXTMIN_API_KEY;
|
|
239
|
+
if (envKey && String(envKey).trim().length > 0) {
|
|
240
|
+
// If environment variable is set, it takes precedence over the database
|
|
241
|
+
this.apiKey = envKey;
|
|
242
|
+
if (doc.apiKey !== envKey) {
|
|
243
|
+
updates.apiKey = envKey;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (doc.apiKey && String(doc.apiKey).trim().length > 0) {
|
|
247
|
+
this.apiKey = doc.apiKey; // honor existing if no env var
|
|
240
248
|
}
|
|
241
249
|
else {
|
|
242
250
|
updates.apiKey = generatedApiKey;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
/**
|
|
3
|
+
* NextMinEvents is a central event emitter for the NextMin ecosystem.
|
|
4
|
+
* It allows components to hook into CRUD and system events.
|
|
5
|
+
*/
|
|
6
|
+
declare class NextMinEvents extends EventEmitter {
|
|
7
|
+
constructor();
|
|
8
|
+
/**
|
|
9
|
+
* Typed emit to provide better DX (Developer Experience)
|
|
10
|
+
*/
|
|
11
|
+
emitEvent(event: string, payload: any): void;
|
|
12
|
+
}
|
|
13
|
+
export declare const events: NextMinEvents;
|
|
14
|
+
export declare const Events: {
|
|
15
|
+
BEFORE_CREATE: string;
|
|
16
|
+
AFTER_CREATE: string;
|
|
17
|
+
BEFORE_UPDATE: string;
|
|
18
|
+
AFTER_UPDATE: string;
|
|
19
|
+
BEFORE_DELETE: string;
|
|
20
|
+
AFTER_DELETE: string;
|
|
21
|
+
BEFORE_READ: string;
|
|
22
|
+
AFTER_READ: string;
|
|
23
|
+
AUTH_LOGIN: string;
|
|
24
|
+
AUTH_LOGOUT: string;
|
|
25
|
+
AUTH_SIGNUP: string;
|
|
26
|
+
SCHEMA_UPDATE: string;
|
|
27
|
+
SERVER_START: string;
|
|
28
|
+
SERVER_STOP: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Helper to generate model-specific event names
|
|
32
|
+
*/
|
|
33
|
+
export declare function getModelEvent(modelName: string, action: 'create' | 'read' | 'update' | 'delete', phase?: 'before' | 'after'): string;
|
|
34
|
+
export {};
|