@airoom/nextmin-node 1.4.5 → 1.4.6
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/api/apiRouter.js +5 -2
- package/dist/api/router/mountCrudRoutes.js +2 -1
- package/dist/api/router/setupAuthRoutes.js +6 -2
- package/dist/policy/authorize.js +23 -5
- package/dist/services/SchemaService.js +4 -4
- package/dist/utils/SchemaLoader.d.ts +2 -2
- package/dist/utils/SchemaLoader.js +13 -15
- package/package.json +1 -1
package/dist/api/apiRouter.js
CHANGED
|
@@ -309,10 +309,13 @@ class APIRouter {
|
|
|
309
309
|
if (this.schemasRouteRegistered)
|
|
310
310
|
return;
|
|
311
311
|
this.schemasRouteRegistered = true;
|
|
312
|
-
this.router.get('/_schemas', this.
|
|
312
|
+
this.router.get('/_schemas', this.optionalAuthMiddleware, async (req, res) => {
|
|
313
|
+
const role = await this.normalizeRoleName(this.getUserRoleFromReq(req));
|
|
314
|
+
// Typically admin/superadmin can see clinical/private fields in schema
|
|
315
|
+
const showPrivate = role === 'admin' || role === 'superadmin';
|
|
313
316
|
res.json({
|
|
314
317
|
success: true,
|
|
315
|
-
data: this.schemaLoader.getPublicSchemaList(),
|
|
318
|
+
data: this.schemaLoader.getPublicSchemaList(showPrivate),
|
|
316
319
|
});
|
|
317
320
|
});
|
|
318
321
|
}
|
|
@@ -430,7 +430,8 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
430
430
|
paged = merged.slice(start, start + limit);
|
|
431
431
|
}
|
|
432
432
|
else {
|
|
433
|
-
|
|
433
|
+
// Get actual total count from database (not just current page length)
|
|
434
|
+
totalRows = await model.count(childFilter);
|
|
434
435
|
paged = merged.slice(0, limit);
|
|
435
436
|
}
|
|
436
437
|
const data = rdec.exposePrivate
|
|
@@ -114,7 +114,7 @@ function setupAuthRoutes(ctx) {
|
|
|
114
114
|
res.json({
|
|
115
115
|
success: true,
|
|
116
116
|
message: 'You are successfully logged in.',
|
|
117
|
-
data: { token, user },
|
|
117
|
+
data: { token, user: { ...user, roleName } },
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
catch (error) {
|
|
@@ -140,8 +140,12 @@ function setupAuthRoutes(ctx) {
|
|
|
140
140
|
.status(404)
|
|
141
141
|
.json({ error: true, message: 'User not found' });
|
|
142
142
|
}
|
|
143
|
+
const roleName = (await ctx.normalizeRoleName(user.role)) ?? '';
|
|
143
144
|
delete user.password;
|
|
144
|
-
return res.json({
|
|
145
|
+
return res.json({
|
|
146
|
+
success: true,
|
|
147
|
+
data: { ...user, roleName },
|
|
148
|
+
});
|
|
145
149
|
}
|
|
146
150
|
catch (error) {
|
|
147
151
|
return res
|
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
|
}
|
|
@@ -117,8 +134,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
117
134
|
// if false → continue to roles/auth checks
|
|
118
135
|
// 3) ROLES
|
|
119
136
|
const roleName = (ctx.role || '').toLowerCase();
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
const roleRules = roleName ? getPolicySection(access, 'roles', roleName) : null;
|
|
138
|
+
if (roleRules) {
|
|
139
|
+
const rule = roleRules[action];
|
|
122
140
|
const rBool = asBool(rule);
|
|
123
141
|
if (rBool === true) {
|
|
124
142
|
const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
|
|
@@ -126,9 +144,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
|
|
|
126
144
|
allow: true,
|
|
127
145
|
readMask: effectiveReadMask,
|
|
128
146
|
writeDeny: effectiveWriteDeny,
|
|
129
|
-
createDefaults: createDefaultsBase
|
|
130
|
-
restrictions: restrictionsBase
|
|
131
|
-
queryFilter: queryFilterBase
|
|
147
|
+
createDefaults: getPolicySection(createDefaultsBase, 'roles', roleName) || {},
|
|
148
|
+
restrictions: getPolicySection(restrictionsBase, 'roles', roleName) || {},
|
|
149
|
+
queryFilter: getPolicySection(queryFilterBase, 'roles', roleName) || {},
|
|
132
150
|
exposePrivate: bypass,
|
|
133
151
|
sensitiveMask,
|
|
134
152
|
};
|
|
@@ -34,13 +34,13 @@ function startSchemaService(server, opts = {}) {
|
|
|
34
34
|
const loader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
|
|
35
35
|
nsp.on('connection', (socket) => {
|
|
36
36
|
Logger_1.default.info('SchemaService:', socket.id);
|
|
37
|
-
// Send
|
|
37
|
+
// Send normalized (secure) snapshot on connect
|
|
38
38
|
socket.emit('schemasData', loader.getPublicSchemaList());
|
|
39
39
|
});
|
|
40
|
-
// Broadcast updates on hot-reload / schema changes
|
|
40
|
+
// Broadcast sanitized updates on hot-reload / schema changes
|
|
41
41
|
if (typeof loader.on === 'function') {
|
|
42
|
-
loader.on('schemasChanged', (
|
|
43
|
-
nsp?.emit('schemasUpdated',
|
|
42
|
+
loader.on('schemasChanged', () => {
|
|
43
|
+
nsp?.emit('schemasUpdated', loader.getPublicSchemaList());
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -33,9 +33,9 @@ export declare class SchemaLoader {
|
|
|
33
33
|
[key: string]: Schema;
|
|
34
34
|
};
|
|
35
35
|
/** CLIENT/API: sanitized map with private attributes removed */
|
|
36
|
-
getPublicSchemas(): Record<string, PublicSchema>;
|
|
36
|
+
getPublicSchemas(showPrivate?: boolean): Record<string, PublicSchema>;
|
|
37
37
|
/** CLIENT/API convenience: array of { modelName, attributes, allowedMethods } */
|
|
38
|
-
getPublicSchemaList(): Array<PublicSchema>;
|
|
38
|
+
getPublicSchemaList(showPrivate?: boolean): Array<PublicSchema>;
|
|
39
39
|
/** Strip any attr marked private; also remove the `private` flag from others */
|
|
40
40
|
/** Keep private/sensitive flags so the UI and policy layer can decide.
|
|
41
41
|
* We only shallow-clone values to avoid leaking references.
|
|
@@ -288,13 +288,13 @@ class SchemaLoader {
|
|
|
288
288
|
return this.schemas;
|
|
289
289
|
}
|
|
290
290
|
/** CLIENT/API: sanitized map with private attributes removed */
|
|
291
|
-
getPublicSchemas() {
|
|
291
|
+
getPublicSchemas(showPrivate = false) {
|
|
292
292
|
const out = {};
|
|
293
293
|
for (const [name, s] of Object.entries(this.schemas)) {
|
|
294
294
|
// if (this.nonOverridableSchemas.has(name)) continue;
|
|
295
295
|
// Clone sanitized attributes and add timestamps
|
|
296
296
|
const attributesWithTimestamps = {
|
|
297
|
-
...this.sanitizeAttributes(s.attributes),
|
|
297
|
+
...this.sanitizeAttributes(s.attributes, showPrivate),
|
|
298
298
|
createdAt: { type: 'datetime' },
|
|
299
299
|
updatedAt: { type: 'datetime' },
|
|
300
300
|
};
|
|
@@ -308,15 +308,15 @@ class SchemaLoader {
|
|
|
308
308
|
return out;
|
|
309
309
|
}
|
|
310
310
|
/** CLIENT/API convenience: array of { modelName, attributes, allowedMethods } */
|
|
311
|
-
getPublicSchemaList() {
|
|
312
|
-
const pub = this.getPublicSchemas();
|
|
311
|
+
getPublicSchemaList(showPrivate = false) {
|
|
312
|
+
const pub = this.getPublicSchemas(showPrivate);
|
|
313
313
|
return Object.values(pub);
|
|
314
314
|
}
|
|
315
315
|
/** Strip any attr marked private; also remove the `private` flag from others */
|
|
316
316
|
/** Keep private/sensitive flags so the UI and policy layer can decide.
|
|
317
317
|
* We only shallow-clone values to avoid leaking references.
|
|
318
318
|
*/
|
|
319
|
-
sanitizeAttributes(attrs) {
|
|
319
|
+
sanitizeAttributes(attrs, showPrivate = false) {
|
|
320
320
|
const out = {};
|
|
321
321
|
if (!attrs || typeof attrs !== 'object' || Array.isArray(attrs))
|
|
322
322
|
return out;
|
|
@@ -325,13 +325,12 @@ class SchemaLoader {
|
|
|
325
325
|
if (Array.isArray(attr)) {
|
|
326
326
|
const elem = attr[0];
|
|
327
327
|
if (elem && typeof elem === 'object') {
|
|
328
|
-
// If the inner descriptor is private, omit this field entirely
|
|
329
|
-
if (elem.private) {
|
|
328
|
+
// If the inner descriptor is private and we're not showing private, omit this field entirely
|
|
329
|
+
if (elem.private && !showPrivate) {
|
|
330
330
|
continue;
|
|
331
331
|
}
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
out[key] = [{ ...rest }];
|
|
332
|
+
// Preserve the descriptor, including the 'private' flag if showPrivate is true
|
|
333
|
+
out[key] = [{ ...elem }];
|
|
335
334
|
}
|
|
336
335
|
else {
|
|
337
336
|
// Fallback: keep as-is (no private flag to check)
|
|
@@ -341,13 +340,12 @@ class SchemaLoader {
|
|
|
341
340
|
}
|
|
342
341
|
// Single attribute object
|
|
343
342
|
if (attr && typeof attr === 'object') {
|
|
344
|
-
// If marked private, omit from public schema entirely
|
|
345
|
-
if (attr.private) {
|
|
343
|
+
// If marked private and we're not showing private, omit from public schema entirely
|
|
344
|
+
if (attr.private && !showPrivate) {
|
|
346
345
|
continue;
|
|
347
346
|
}
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
out[key] = { ...rest };
|
|
347
|
+
// Preserve the descriptor, including the 'private' flag if showPrivate is true
|
|
348
|
+
out[key] = { ...attr };
|
|
351
349
|
continue;
|
|
352
350
|
}
|
|
353
351
|
// Unexpected primitives — pass through
|