webspresso 0.0.75 → 0.0.76

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
@@ -2165,10 +2165,34 @@ const { app } = createApp({
2165
2165
  ```
2166
2166
 
2167
2167
  - **ORM:** `zdb.file({ maxLength: 2048, nullable: true })` — string column for the stored public URL or path; migrations use `table.string(..., maxLength)`.
2168
- - **Admin:** the panel reads **`settings.uploadUrl`** from the registry (set automatically when `uploadPlugin` is registered **before** `adminPanelPlugin`, or pass **`adminPanelPlugin({ uploadUrl: '/api/upload' })`**). File fields (`type: 'file'` or `customFields` type `file-upload`) POST to that URL with credentials.
2168
+ - **Admin forms:** columns with `zdb.file()` automatically render a drag-and-drop upload widget (or a manual URL text field when `uploadUrl` is not configured). Optional `ui: { label, hint, accept, maxBytes }` on the column customizes the widget. For existing `zdb.string()` columns you can use `admin.customFields: { columnName: { type: 'file-upload' } }` instead of changing the schema type.
2169
+ - **Admin:** the panel reads **`settings.uploadUrl`** from the registry (set automatically when `uploadPlugin` is registered **before** `adminPanelPlugin`, or pass **`adminPanelPlugin({ uploadUrl: '/api/upload' })`**). File fields (`type: 'file'` or `customFields` type `file-upload`) POST to that URL with credentials; the saved record stores the returned **`url`** / **`publicUrl`** string.
2169
2170
  - **Response:** `{ url, publicUrl, key? }` — clients typically persist **`url`** / **`publicUrl`** in the model.
2170
2171
  - **Custom storage:** `uploadPlugin({ provider: { async put({ buffer, originalName, mimeType, size, req }) { return { publicUrl: '...' }; } } })`.
2171
2172
 
2173
+ **Admin model example** (`zdb.file()` picks up the upload widget from schema; no extra `customFields` needed):
2174
+
2175
+ ```javascript
2176
+ const { defineModel, zdb } = require('webspresso');
2177
+
2178
+ const Post = defineModel({
2179
+ name: 'Post',
2180
+ table: 'posts',
2181
+ schema: zdb.schema({
2182
+ id: zdb.id(),
2183
+ title: zdb.string(),
2184
+ cover_image: zdb.file({
2185
+ maxLength: 2048,
2186
+ nullable: true,
2187
+ ui: { label: 'Cover', accept: 'image/*', hint: 'JPEG or PNG' },
2188
+ }),
2189
+ }),
2190
+ admin: { enabled: true, label: 'Posts' },
2191
+ });
2192
+ ```
2193
+
2194
+ For a plain string column, use `admin.customFields: { attachment: { type: 'file-upload' } }` instead of `zdb.file()`.
2195
+
2172
2196
  ### Health check plugin
2173
2197
 
2174
2198
  Exposes a lightweight **GET** endpoint for load balancers and orchestrators (Kubernetes, Docker healthcheck, etc.). **Enabled by default** in all environments; set `enabled: false` to turn it off.
@@ -3,7 +3,8 @@
3
3
  * Reset admin user password via CLI
4
4
  */
5
5
 
6
- const readline = require('readline');
6
+ const inquirer = require('inquirer');
7
+ const { hash } = require('../../core/auth/hash');
7
8
  const { loadDbConfig, createDbInstance } = require('../utils/db');
8
9
 
9
10
  function registerCommand(program) {
@@ -32,17 +33,14 @@ function registerCommand(program) {
32
33
  // Get email (interactive if not provided)
33
34
  let email = options.email;
34
35
  if (!email) {
35
- const rl = readline.createInterface({
36
- input: process.stdin,
37
- output: process.stdout,
38
- });
39
-
40
- email = await new Promise((resolve) => {
41
- rl.question('Enter admin email: ', (answer) => {
42
- rl.close();
43
- resolve(answer.trim());
44
- });
45
- });
36
+ const answers = await inquirer.prompt([
37
+ {
38
+ type: 'input',
39
+ name: 'email',
40
+ message: 'Enter admin email:',
41
+ },
42
+ ]);
43
+ email = answers.email.trim();
46
44
  }
47
45
 
48
46
  if (!email) {
@@ -70,49 +68,15 @@ function registerCommand(program) {
70
68
  // Get new password (interactive if not provided)
71
69
  let password = options.password;
72
70
  if (!password) {
73
- const rl = readline.createInterface({
74
- input: process.stdin,
75
- output: process.stdout,
76
- });
77
-
78
- // Disable echo for password input
79
- if (process.stdin.isTTY) {
80
- process.stdout.write('Enter new password: ');
81
- password = await new Promise((resolve) => {
82
- let pwd = '';
83
- process.stdin.setRawMode(true);
84
- process.stdin.resume();
85
- process.stdin.on('data', (char) => {
86
- char = char.toString();
87
- if (char === '\n' || char === '\r') {
88
- process.stdin.setRawMode(false);
89
- process.stdin.pause();
90
- console.log(); // New line after password
91
- resolve(pwd);
92
- } else if (char === '\u0003') {
93
- // Ctrl+C
94
- process.exit();
95
- } else if (char === '\u007F') {
96
- // Backspace
97
- if (pwd.length > 0) {
98
- pwd = pwd.slice(0, -1);
99
- process.stdout.write('\b \b');
100
- }
101
- } else {
102
- pwd += char;
103
- process.stdout.write('*');
104
- }
105
- });
106
- });
107
- rl.close();
108
- } else {
109
- password = await new Promise((resolve) => {
110
- rl.question('Enter new password: ', (answer) => {
111
- rl.close();
112
- resolve(answer);
113
- });
114
- });
115
- }
71
+ const answers = await inquirer.prompt([
72
+ {
73
+ type: 'password',
74
+ name: 'password',
75
+ message: 'Enter new password:',
76
+ mask: '*',
77
+ },
78
+ ]);
79
+ password = answers.password;
116
80
  }
117
81
 
118
82
  if (!password || password.length < 6) {
@@ -121,8 +85,8 @@ function registerCommand(program) {
121
85
  process.exit(1);
122
86
  }
123
87
 
124
- // Hash the password
125
- const hashedPassword = await bcrypt.hash(password, 10);
88
+ // Hash the password (same rounds as admin panel setup)
89
+ const hashedPassword = await hash(password, 10);
126
90
 
127
91
  // Update the password
128
92
  await db('admin_users')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.75",
3
+ "version": "0.0.76",
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
  "types": "index.d.ts",
@@ -169,6 +169,7 @@ Analytics plugin adds `fsy.analyticsHead`, `fsy.verificationTags`, etc., when co
169
169
  - **Relations:** `belongsTo`, `hasMany`, `hasOne` with `model: () => OtherModel`.
170
170
  - **Scopes:** `softDelete`, `timestamps`, optional `tenant` column.
171
171
  - **`hidden`:** columns never exposed in admin/API (e.g. `password_hash`).
172
+ - **`zdb.file()`:** varchar column for uploaded asset URL/path; admin forms render a file upload widget when `uploadPlugin` is registered (or `adminPanelPlugin({ uploadUrl })`). Optional `ui: { label, accept, maxBytes }`. For `zdb.string()` columns use `admin.customFields: { col: { type: 'file-upload' } }`.
172
173
  - **Nanoid PK:** `zdb.nanoid()` / `zdb.nanoid({ maxLength: 12 })` — string primary key; migrations use `string(length)`. On **`create()`**, omitting the PK auto-fills a URL-safe id (built-in generator, same alphabet as `nanoid`). Use **`zdb.foreignNanoid('table', { maxLength })`** when the parent uses nanoid PKs; **`generateNanoid`** is exported from `webspresso` for manual ids. In API **`schema`**, use **`z.nanoid()`** / **`z.nanoid(12)`** / **`z.nanoid({ maxLength })`** (the `z` from `schema: ({ z })` is extended by Webspresso). **`zodNanoid`** / **`extendZ`** are also exported for non-route use.
173
174
 
174
175
  **Database:** `createDatabase({ client, connection, models: './models' })` — auto-loads `models/*.js` (ignore `_prefix`).
@@ -197,7 +198,7 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
197
198
  | `adminPanelPlugin` | SPA admin CRUD — needs **`db`**; optional **`uploadUrl`** (or infer from **`uploadPlugin`**); optional **`userManagement: { enabled, model, fields }`** + **`auth`** (same **`AuthManager`** as **`createApp({ auth })`**) for site-user CRUD + remember-me session UI — see **Session authentication** above |
198
199
  | `dataExchangePlugin` | Admin-only **Excel export** + **CSV/XLSX import** under `${adminPath}/api/data-exchange/*`; register **after** `adminPanelPlugin` with same `db` / `adminPath`; optional `maxRows`, `maxFileBytes`; adds UI buttons + bulk `export-xlsx` |
199
200
  | `redirectPlugin` | Configurable **301–308** redirects in `register()` — runs **before** file-based SSR routes; `rules` (`from` path or `RegExp`, `to`, `status`, `methods`), `preserveQuery`, `allowExternal`, `trailingSlash`, `defaultMethods`; docs **[`doc/index.html#plugins-redirect`](../../../doc/index.html#plugins-redirect)**, README **Redirect plugin** |
200
- | `uploadPlugin` | `POST` multipart (`multer`), `createLocalFileProvider` or custom `provider`; set **`mimeAllowlist`** / **`maxBytes`** in production |
201
+ | `uploadPlugin` | `POST` multipart (`multer`), `createLocalFileProvider` or custom `provider`; set **`mimeAllowlist`** / **`maxBytes`** in production; pairs with admin **`zdb.file()`** / **`customFields.file-upload`** when registered before **`adminPanelPlugin`** |
201
202
  | `siteAnalyticsPlugin` | Self-hosted page views + admin charts |
202
203
  | `auditLogPlugin` | Admin mutation audit trail |
203
204
  | `recaptchaPlugin` | v2/v3 + middleware |