webspresso 0.0.51 → 0.0.52

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 CHANGED
@@ -1164,9 +1164,13 @@ const User = defineModel({
1164
1164
  timestamps: true, // Auto-manage created_at/updated_at
1165
1165
  tenant: 'tenant_id', // Multi-tenant column (optional)
1166
1166
  },
1167
+
1168
+ hidden: ['password_hash', 'api_token'], // Never expose in API/templates (security)
1167
1169
  });
1168
1170
  ```
1169
1171
 
1172
+ **Hidden columns:** Add column names to `hidden` so they are never exposed in admin API responses, exports, or when passing to templates. Use for sensitive data like `password_hash`, `api_token`, `secret_key`. The admin panel will exclude these from list views and forms automatically.
1173
+
1170
1174
  ### Auto-Loading Models
1171
1175
 
1172
1176
  Models are automatically loaded from the `models/` directory when you create a database instance:
package/core/orm/index.js CHANGED
@@ -13,6 +13,7 @@ const { createMigrationManager } = require('./migrations');
13
13
  const { createSeeder } = require('./seeder');
14
14
  const { createScopeContext } = require('./scopes');
15
15
  const { ModelEvents, Hooks, HookCancellationError, createEventContext } = require('./events');
16
+ const { omitHiddenColumns, sanitizeForOutput } = require('./utils');
16
17
 
17
18
  /**
18
19
  * Create a database instance
@@ -272,6 +273,9 @@ module.exports = {
272
273
  // Column utilities
273
274
  extractColumnsFromSchema,
274
275
  getColumnMeta,
276
+ // Output sanitization (exclude hidden columns from API/templates)
277
+ omitHiddenColumns,
278
+ sanitizeForOutput,
275
279
  // Events/Signals
276
280
  ModelEvents,
277
281
  Hooks,
package/core/orm/model.js CHANGED
@@ -28,6 +28,7 @@ function defineModel(options) {
28
28
  scopes = {},
29
29
  admin = {},
30
30
  hooks = {},
31
+ hidden = [],
31
32
  } = options;
32
33
 
33
34
  // Validate required fields
@@ -88,6 +89,7 @@ function defineModel(options) {
88
89
  customFields: admin.customFields || {},
89
90
  queries: admin.queries || {},
90
91
  },
92
+ hidden: Array.isArray(hidden) ? hidden : [],
91
93
  hooks: {},
92
94
  };
93
95
 
package/core/orm/types.js CHANGED
@@ -147,6 +147,7 @@
147
147
  * @property {RelationsMap} [relations={}] - Relation definitions
148
148
  * @property {ScopeOptions} [scopes={}] - Scope options
149
149
  * @property {AdminMetadata} [admin] - Admin panel metadata
150
+ * @property {string[]} [hidden=[]] - Column names to never expose in API/templates (e.g. password_hash, api_token)
150
151
  */
151
152
 
152
153
  /**
@@ -159,6 +160,7 @@
159
160
  * @property {ScopeOptions} scopes - Scope options
160
161
  * @property {Map<string, ColumnMeta>} columns - Parsed column metadata
161
162
  * @property {AdminMetadata} [admin] - Admin panel metadata
163
+ * @property {string[]} hidden - Column names never exposed in API/templates
162
164
  */
163
165
 
164
166
  // ============================================================================
package/core/orm/utils.js CHANGED
@@ -114,9 +114,37 @@ function deepClone(obj) {
114
114
  return cloned;
115
115
  }
116
116
 
117
+ /**
118
+ * Remove hidden columns from a record for safe API/template output
119
+ * @param {Object} record - Record from database
120
+ * @param {import('./types').ModelDefinition} model - Model definition with hidden columns
121
+ * @returns {Object} Record without hidden columns
122
+ */
123
+ function omitHiddenColumns(record, model) {
124
+ if (!record) return record;
125
+ if (!model?.hidden?.length) return record;
126
+ return omit(record, model.hidden);
127
+ }
128
+
129
+ /**
130
+ * Remove hidden columns from records (array or single) for safe output
131
+ * @param {Object|Object[]} records - Record(s) from database
132
+ * @param {import('./types').ModelDefinition} model - Model definition
133
+ * @returns {Object|Object[]} Sanitized record(s)
134
+ */
135
+ function sanitizeForOutput(records, model) {
136
+ if (!model?.hidden?.length) return records;
137
+ if (Array.isArray(records)) {
138
+ return records.map((r) => omit(r, model.hidden));
139
+ }
140
+ return omit(records, model.hidden);
141
+ }
142
+
117
143
  module.exports = {
118
144
  pick,
119
145
  omit,
146
+ omitHiddenColumns,
147
+ sanitizeForOutput,
120
148
  formatDateForDb,
121
149
  generateMigrationTimestamp,
122
150
  snakeToCamel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -33,6 +33,7 @@ function createAdminUserModel() {
33
33
  scopes: {
34
34
  timestamps: true,
35
35
  },
36
+ hidden: ['password'], // Never expose in API/templates
36
37
  });
37
38
  }
38
39
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const { getAllModels, getModel } = require('../../core/orm/model');
8
+ const { sanitizeForOutput } = require('../../core/orm/utils');
8
9
  const { checkAdminExists, setupAdmin, login, logout, requireAuth } = require('./auth');
9
10
 
10
11
  /**
@@ -198,9 +199,11 @@ function createApiHandlers(options) {
198
199
  return res.status(403).json({ error: 'Model not enabled in admin panel' });
199
200
  }
200
201
 
201
- // Build column metadata
202
+ // Build column metadata (hidden columns excluded from list/forms for security)
203
+ const hiddenSet = new Set(model.hidden || []);
202
204
  const columns = [];
203
205
  for (const [name, meta] of model.columns) {
206
+ const isHidden = hiddenSet.has(name);
204
207
  columns.push({
205
208
  name,
206
209
  type: meta.type,
@@ -214,7 +217,8 @@ function createApiHandlers(options) {
214
217
  autoIncrement: meta.autoIncrement || false,
215
218
  customField: model.admin.customFields?.[name] || null,
216
219
  validations: meta.validations || null,
217
- ui: meta.ui || null,
220
+ ui: meta.ui ? { ...meta.ui, hidden: isHidden || meta.ui.hidden } : (isHidden ? { hidden: true } : null),
221
+ hidden: isHidden, // Excluded from list display and API responses
218
222
  });
219
223
  }
220
224
 
@@ -394,7 +398,7 @@ function createApiHandlers(options) {
394
398
  const records = await query.list();
395
399
 
396
400
  res.json({
397
- data: records,
401
+ data: sanitizeForOutput(records, model),
398
402
  pagination: {
399
403
  page,
400
404
  perPage,
@@ -426,7 +430,7 @@ function createApiHandlers(options) {
426
430
  return res.status(404).json({ error: 'Record not found' });
427
431
  }
428
432
 
429
- res.json({ data: record });
433
+ res.json({ data: sanitizeForOutput(record, model) });
430
434
  } catch (error) {
431
435
  res.status(500).json({ error: error.message });
432
436
  }
@@ -459,7 +463,7 @@ function createApiHandlers(options) {
459
463
  const repo = db.getRepository(model.name);
460
464
  const record = await repo.create(req.body);
461
465
 
462
- res.status(201).json({ data: record });
466
+ res.status(201).json({ data: sanitizeForOutput(record, model) });
463
467
  } catch (error) {
464
468
  res.status(400).json({ error: error.message });
465
469
  }
@@ -496,7 +500,7 @@ function createApiHandlers(options) {
496
500
  return res.status(404).json({ error: 'Record not found' });
497
501
  }
498
502
 
499
- res.json({ data: record });
503
+ res.json({ data: sanitizeForOutput(record, model) });
500
504
  } catch (error) {
501
505
  res.status(400).json({ error: error.message });
502
506
  }
@@ -550,7 +554,7 @@ function createApiHandlers(options) {
550
554
  return res.status(404).json({ error: 'Record not found in trash' });
551
555
  }
552
556
 
553
- res.json({ success: true, data: record });
557
+ res.json({ success: true, data: sanitizeForOutput(record, model) });
554
558
  } catch (error) {
555
559
  res.status(500).json({ error: error.message });
556
560
  }
@@ -579,7 +583,7 @@ function createApiHandlers(options) {
579
583
  // Get all related records (for dropdown/select)
580
584
  const records = await relatedRepo.findAll();
581
585
 
582
- res.json({ data: records });
586
+ res.json({ data: sanitizeForOutput(records, relatedModel) });
583
587
  } catch (error) {
584
588
  res.status(500).json({ error: error.message });
585
589
  }
@@ -1484,12 +1484,15 @@ const BulkFieldUpdateDropdown = {
1484
1484
  // Get columns to display in table (limit to reasonable number)
1485
1485
  function getDisplayColumns(columns) {
1486
1486
  if (!columns || columns.length === 0) return [];
1487
-
1487
+
1488
+ // Filter out hidden columns (password_hash, api_token, etc.)
1489
+ const visible = [...columns].filter((col) => !col.hidden);
1490
+
1488
1491
  // Prioritize: id, name/title, then others (excluding long text/json fields)
1489
1492
  const priority = ['id', 'name', 'title', 'email', 'slug', 'status', 'published', 'created_at'];
1490
1493
  const exclude = ['password', 'content', 'body', 'description']; // Usually too long
1491
-
1492
- const sorted = [...columns].sort((a, b) => {
1494
+
1495
+ const sorted = visible.sort((a, b) => {
1493
1496
  const aIdx = priority.indexOf(a.name);
1494
1497
  const bIdx = priority.indexOf(b.name);
1495
1498
  if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
@@ -4,6 +4,8 @@
4
4
  * @module plugins/admin-panel/core/api-extensions
5
5
  */
6
6
 
7
+ const { sanitizeForOutput } = require('../../../core/orm/utils');
8
+
7
9
  /**
8
10
  * Build query with filters applied
9
11
  * @param {Object} repo - Repository instance
@@ -573,8 +575,9 @@ function createExtensionApiHandlers(options) {
573
575
  }
574
576
 
575
577
  if (format === 'csv') {
576
- // CSV export
577
- const columns = Array.from(model.columns.keys());
578
+ // CSV export (exclude hidden columns)
579
+ const hiddenSet = new Set(model.hidden || []);
580
+ const columns = Array.from(model.columns.keys()).filter((c) => !hiddenSet.has(c));
578
581
  const header = columns.join(',');
579
582
  const rows = records.map(record => {
580
583
  return columns.map(col => {
@@ -595,8 +598,8 @@ function createExtensionApiHandlers(options) {
595
598
  res.setHeader('Content-Disposition', `attachment; filename="${modelName}_export.csv"`);
596
599
  res.json({ data: csvContent, format: 'csv' });
597
600
  } else {
598
- // JSON export
599
- res.json({ data: records, model: modelName, exportedAt: new Date().toISOString() });
601
+ // JSON export (exclude hidden columns)
602
+ res.json({ data: sanitizeForOutput(records, model), model: modelName, exportedAt: new Date().toISOString() });
600
603
  }
601
604
  } catch (error) {
602
605
  console.error('Export error:', error);