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 +4 -2
- package/bin/commands/upgrade.js +146 -0
- package/bin/webspresso.js +2 -0
- package/package.json +1 -1
- package/plugins/admin-panel/app.js +109 -0
- package/plugins/admin-panel/components.js +111 -111
- package/plugins/admin-panel/modules/dashboard.js +16 -13
- package/plugins/admin-panel/modules/user-management.js +32 -11
- package/plugins/data-exchange/export-xlsx.js +3 -0
- package/plugins/data-exchange/record-selection.js +21 -5
- package/plugins/site-analytics/admin-component.js +88 -78
- package/src/file-router.js +80 -4
- package/src/server.js +2 -0
- package/templates/skills/webspresso-usage/SKILL.md +5 -2
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` -
|
|
768
|
-
- `userManagement` -
|
|
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
|
@@ -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();
|