@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.
@@ -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.apiKeyMiddleware, (_req, res) => {
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
- totalRows = merged.length;
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({ success: true, data: user });
145
+ return res.json({
146
+ success: true,
147
+ data: { ...user, roleName },
148
+ });
145
149
  }
146
150
  catch (error) {
147
151
  return res
@@ -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
- if (roleName && access?.roles?.[roleName]) {
121
- const rule = access.roles[roleName][action];
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?.roles?.[roleName] || {},
130
- restrictions: restrictionsBase?.roles?.[roleName] || {},
131
- queryFilter: queryFilterBase?.roles?.[roleName] || {},
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 full snapshot on connect
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', (schemas) => {
43
- nsp?.emit('schemasUpdated', schemas);
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 from public schema
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
- // Keep as an array with a single shallow-cloned descriptor (without leaking private flag)
333
- const { private: _omit, ...rest } = elem;
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
- // Shallow clone and drop the private flag if present
349
- const { private: _omit, ...rest } = attr;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",