webspresso 0.0.72 → 0.0.74

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
@@ -764,10 +764,12 @@ const { app } = createApp({
764
764
  Options:
765
765
  - `db` (required) - Database instance
766
766
  - `path` - Admin panel path (default: `/_admin`)
767
- - `auth` - Auth manager for user management
768
- - `userManagement` - User management config (`enabled`, `model`, `fields`)
767
+ - `auth` - Same **`AuthManager`** instance as **`createApp({ auth })`** when you use **`userManagement`** — enables **Active Sessions** / revoke APIs if **`rememberTokens`** (remember-me) is configured; optional for user CRUD-only
768
+ - `userManagement` - Site-user admin UI (`enabled`, `model` matching ORM user table, optional `fields` map). SPA routes: `/_admin/users`, `/_admin/users/new`, …; APIs: `/_admin/api/users*`. Admin staff still use **`admin_users`** / `/_admin` login; this is separate from **`req.user`** on the public site
769
769
  - `configure` - Callback `(registry) => void` for manual setup
770
770
 
771
+ See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.cursor/skills/webspresso-usage/SKILL.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
772
+
771
773
  **Custom Admin Pages (registerModule):**
772
774
 
773
775
  Plugins can add custom admin pages using `registerModule` in `onRoutesReady`:
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Upgrade Command — bump the webspresso dependency in the current project
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { execSync } = require('child_process');
8
+
9
+ const PKG_NAME = 'webspresso';
10
+
11
+ /**
12
+ * @param {string} cwd
13
+ * @returns {'npm'|'pnpm'|'yarn'}
14
+ */
15
+ function detectPackageManager(cwd) {
16
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
17
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
18
+ return 'npm';
19
+ }
20
+
21
+ /**
22
+ * @param {object} pkg
23
+ * @returns {{ specifier: string, saveDev: boolean } | null}
24
+ */
25
+ function findWebspressoDep(pkg) {
26
+ if (!pkg || typeof pkg !== 'object') return null;
27
+ const prod = pkg.dependencies && pkg.dependencies[PKG_NAME];
28
+ const dev = pkg.devDependencies && pkg.devDependencies[PKG_NAME];
29
+ if (prod) return { specifier: String(prod), saveDev: false };
30
+ if (dev) return { specifier: String(dev), saveDev: true };
31
+ return null;
32
+ }
33
+
34
+ function isNonRegistrySpecifier(spec) {
35
+ return /^(file:|link:|workspace:|git\+|github:|http:|https:)/i.test(spec.trim());
36
+ }
37
+
38
+ /**
39
+ * @param {'npm'|'pnpm'|'yarn'} pm
40
+ * @param {string} tag
41
+ * @param {boolean} saveDev
42
+ * @returns {string[]}
43
+ */
44
+ function buildInstallArgs(pm, tag, saveDev) {
45
+ const spec = `${PKG_NAME}@${tag}`;
46
+ if (pm === 'npm') {
47
+ return saveDev
48
+ ? ['install', spec, '--save-dev']
49
+ : ['install', spec, '--save'];
50
+ }
51
+ if (pm === 'pnpm') {
52
+ return saveDev ? ['add', '-D', spec] : ['add', spec];
53
+ }
54
+ /* yarn */
55
+ return saveDev ? ['add', '-D', spec] : ['add', spec];
56
+ }
57
+
58
+ function registerCommand(program) {
59
+ program
60
+ .command('upgrade')
61
+ .description(
62
+ 'Upgrade the webspresso package in the current project (npm/pnpm/yarn)'
63
+ )
64
+ .option(
65
+ '-t, --tag <dist-tag>',
66
+ 'npm dist-tag (e.g. latest, next)',
67
+ 'latest'
68
+ )
69
+ .option('--pm <manager>', 'Force package manager: npm, pnpm, or yarn')
70
+ .option('--dry-run', 'Print the install command without running it')
71
+ .action((options) => {
72
+ const cwd = process.cwd();
73
+ const pkgPath = path.join(cwd, 'package.json');
74
+
75
+ if (!fs.existsSync(pkgPath)) {
76
+ console.error('❌ package.json not found. Run this from your project root.');
77
+ process.exit(1);
78
+ }
79
+
80
+ let pkg;
81
+ try {
82
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
83
+ } catch (e) {
84
+ console.error(`❌ Could not read package.json: ${e.message}`);
85
+ process.exit(1);
86
+ }
87
+
88
+ const found = findWebspressoDep(pkg);
89
+ if (!found) {
90
+ console.error(
91
+ `❌ No "${PKG_NAME}" entry in dependencies or devDependencies.`
92
+ );
93
+ console.error(' Add it first, e.g. npm install webspresso');
94
+ process.exit(1);
95
+ }
96
+
97
+ if (isNonRegistrySpecifier(found.specifier)) {
98
+ console.error(
99
+ `❌ "${PKG_NAME}" is not a registry semver range (current: ${found.specifier}).`
100
+ );
101
+ console.error(
102
+ ' For file:/link:/workspace: installs, change package.json manually or point to a published version.'
103
+ );
104
+ process.exit(1);
105
+ }
106
+
107
+ let pm = options.pm;
108
+ if (pm) {
109
+ pm = String(pm).toLowerCase();
110
+ if (!['npm', 'pnpm', 'yarn'].includes(pm)) {
111
+ console.error('❌ --pm must be npm, pnpm, or yarn');
112
+ process.exit(1);
113
+ }
114
+ } else {
115
+ pm = detectPackageManager(cwd);
116
+ }
117
+
118
+ const args = buildInstallArgs(pm, options.tag, found.saveDev);
119
+ const cmd = `${pm} ${args.join(' ')}`;
120
+
121
+ console.log('\nWebspresso upgrade');
122
+ console.log('==================\n');
123
+ console.log(` Package manager: ${pm}`);
124
+ console.log(` Target: ${PKG_NAME}@${options.tag}`);
125
+ console.log(` Save as: ${found.saveDev ? 'devDependency' : 'dependency'}`);
126
+ console.log(` Current range: ${found.specifier}\n`);
127
+
128
+ if (options.dryRun) {
129
+ console.log(`Dry run — would run:\n ${cmd}\n`);
130
+ return;
131
+ }
132
+
133
+ try {
134
+ execSync(cmd, { stdio: 'inherit', cwd, shell: true });
135
+ console.log(
136
+ '\n✅ Upgrade finished. If you use native addons (better-sqlite3, bcrypt, sharp), run:\n' +
137
+ ' npm run rebuild:native\n' +
138
+ ' (or: npm rebuild better-sqlite3 bcrypt sharp)\n'
139
+ );
140
+ } catch {
141
+ process.exit(1);
142
+ }
143
+ });
144
+ }
145
+
146
+ module.exports = { registerCommand, detectPackageManager, findWebspressoDep };
package/bin/webspresso.js CHANGED
@@ -30,6 +30,7 @@ const { registerCommand: registerFaviconGenerate } = require('./commands/favicon
30
30
  const { registerCommand: registerAuditPrune } = require('./commands/audit-prune');
31
31
  const { registerCommand: registerDoctor } = require('./commands/doctor');
32
32
  const { registerCommand: registerSkill } = require('./commands/skill');
33
+ const { registerCommand: registerUpgrade } = require('./commands/upgrade');
33
34
 
34
35
  registerNew(program);
35
36
  registerPage(program);
@@ -48,6 +49,7 @@ registerFaviconGenerate(program);
48
49
  registerAuditPrune(program);
49
50
  registerDoctor(program);
50
51
  registerSkill(program);
52
+ registerUpgrade(program);
51
53
 
52
54
  // Parse arguments
53
55
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.72",
3
+ "version": "0.0.74",
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",
@@ -35,6 +35,87 @@ async function checkSetupNeeded() {
35
35
  }
36
36
  }
37
37
 
38
+ // Site user management: menu uses /models/{userModel}*; /users* kept as aliases (redirect) + /users/sessions
39
+ function getUserManagementModel() {
40
+ var um = window.__ADMIN_CONFIG__ && window.__ADMIN_CONFIG__.userManagement;
41
+ if (!um || !um.enabled) return null;
42
+ return um.model || 'User';
43
+ }
44
+
45
+ async function guardUserManagementRoutes() {
46
+ var isAuth = await checkAuth();
47
+ if (!isAuth) {
48
+ m.route.set('/login');
49
+ return false;
50
+ }
51
+ if (!getUserManagementModel()) {
52
+ m.route.set('/');
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ const UserSessionsPage = {
59
+ oninit: async function (vnode) {
60
+ vnode.state.loading = true;
61
+ vnode.state.rows = [];
62
+ vnode.state.message = null;
63
+ vnode.state.error = null;
64
+ try {
65
+ var res = await api.get('/users/sessions');
66
+ vnode.state.rows = res.data || [];
67
+ vnode.state.message = res.message || null;
68
+ } catch (e) {
69
+ vnode.state.error = e.message || String(e);
70
+ }
71
+ vnode.state.loading = false;
72
+ m.redraw();
73
+ },
74
+ view: function (vnode) {
75
+ var rows = vnode.state.rows || [];
76
+ var umModel = getUserManagementModel();
77
+ var usersCrudHref = umModel ? '/models/' + encodeURIComponent(umModel) : '/';
78
+ return m(Layout, { breadcrumbs: [{ label: 'Users', href: usersCrudHref }, { label: 'Active Sessions', href: '/users/sessions' }] }, [
79
+ m('h2.text-2xl.font-bold.mb-4.text-gray-900.dark:text-slate-100', 'Active Sessions'),
80
+ vnode.state.loading ? m('p.text-gray-600.dark:text-slate-400', 'Loading…') : null,
81
+ vnode.state.error ? m('p.text-red-600.dark:text-red-400.mb-2', vnode.state.error) : null,
82
+ vnode.state.message ? m('p.text-sm.text-gray-600.dark:text-slate-400.mb-2', vnode.state.message) : null,
83
+ !vnode.state.loading && rows.length === 0 && !vnode.state.error
84
+ ? m('p.text-gray-600.dark:text-slate-400', 'No remember-me sessions (or tracking not enabled).')
85
+ : m('.overflow-x-auto.rounded-lg.border.border-gray-200.dark:border-slate-700.bg-white.dark:bg-slate-800/50', [
86
+ m('table.min-w-full.text-sm.text-left', [
87
+ m('thead.bg-gray-50.dark:bg-slate-900', m('tr', [
88
+ m('th.px-3.py-2.text-xs.font-medium.text-gray-500.dark:text-slate-400.uppercase.tracking-wider', 'User'),
89
+ m('th.px-3.py-2.text-xs.font-medium.text-gray-500.dark:text-slate-400.uppercase.tracking-wider', 'Token (prefix)'),
90
+ m('th.px-3.py-2.text-xs.font-medium.text-gray-500.dark:text-slate-400.uppercase.tracking-wider', 'Created'),
91
+ m('th.px-3.py-2', ''),
92
+ ])),
93
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700', rows.map(function (r) {
94
+ var tok = (r.token || '').slice(0, 12) + '…';
95
+ return m('tr.bg-white.dark:bg-slate-800/30', [
96
+ m('td.px-3.py-2.text-gray-900.dark:text-slate-200', (r.user_email || r.user_id || '') + ''),
97
+ m('td.px-3.py-2.font-mono.text-xs.text-gray-800.dark:text-slate-300', tok),
98
+ m('td.px-3.py-2.text-gray-700.dark:text-slate-300', r.created_at ? new Date(r.created_at).toLocaleString() : ''),
99
+ m('td.px-3.py-2', m('button.text-red-600.dark:text-red-400.text-xs', {
100
+ onclick: async function () {
101
+ if (!confirm('Revoke this session?')) return;
102
+ try {
103
+ await api.delete('/users/sessions/' + encodeURIComponent(r.token));
104
+ vnode.state.rows = vnode.state.rows.filter(function (x) { return x.token !== r.token; });
105
+ m.redraw();
106
+ } catch (err) {
107
+ alert(err.message || err);
108
+ }
109
+ },
110
+ }, 'Revoke')),
111
+ ]);
112
+ })),
113
+ ]),
114
+ ]),
115
+ ]);
116
+ },
117
+ };
118
+
38
119
  // Build routes
39
120
  var routes = {
40
121
  '/': {
@@ -80,6 +161,34 @@ var routes = {
80
161
  return SettingsPage;
81
162
  }
82
163
  },
164
+ '/users/sessions': {
165
+ onmatch: async () => {
166
+ if (!(await guardUserManagementRoutes())) return;
167
+ return UserSessionsPage;
168
+ }
169
+ },
170
+ '/users/new': {
171
+ onmatch: async () => {
172
+ if (!(await guardUserManagementRoutes())) return;
173
+ var model = getUserManagementModel();
174
+ m.route.set('/models/' + encodeURIComponent(model) + '/new');
175
+ }
176
+ },
177
+ '/users/:id/edit': {
178
+ onmatch: async () => {
179
+ if (!(await guardUserManagementRoutes())) return;
180
+ var model = getUserManagementModel();
181
+ var id = m.route.param('id');
182
+ m.route.set('/models/' + encodeURIComponent(model) + '/edit/' + encodeURIComponent(id));
183
+ }
184
+ },
185
+ '/users': {
186
+ onmatch: async () => {
187
+ if (!(await guardUserManagementRoutes())) return;
188
+ var model = getUserManagementModel();
189
+ m.route.set('/models/' + encodeURIComponent(model));
190
+ }
191
+ },
83
192
  '/models/:model': {
84
193
  onmatch: async () => {
85
194
  const isAuth = await checkAuth();