webspresso 0.0.83 → 0.0.85

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.
@@ -28,7 +28,7 @@ function createAuthMiddleware(authManager) {
28
28
 
29
29
  /**
30
30
  * Authenticate middleware
31
- * Loads user from session or remember me token
31
+ * Loads user from Bearer JWT header, session, or remember me token
32
32
  * Should run on every request
33
33
  */
34
34
  async function authenticate(req, res, next) {
@@ -39,8 +39,27 @@ function createAuthMiddleware(authManager) {
39
39
  // Create request-bound auth helper
40
40
  req.auth = authManager.createRequestAuth(req, res);
41
41
 
42
- // Try to load user from session
43
- if (req.session?.userId) {
42
+ // Check Bearer JWT token header first
43
+ const authHeader = req.headers?.authorization || req.headers?.Authorization;
44
+ if (authHeader && authHeader.startsWith('Bearer ')) {
45
+ const token = authHeader.substring(7).trim();
46
+ try {
47
+ const payload = authManager.verifyJwt(token);
48
+ req.token = token;
49
+ req.jwtPayload = payload;
50
+ if (payload.id) {
51
+ const user = await authManager.findUserById(payload.id);
52
+ req.user = user || payload;
53
+ } else {
54
+ req.user = payload;
55
+ }
56
+ } catch (jwtErr) {
57
+ // Invalid or expired JWT token header
58
+ }
59
+ }
60
+
61
+ // Try to load user from session if not authenticated by JWT
62
+ if (!req.user && req.session?.userId) {
44
63
  const user = await authManager.findUserById(req.session.userId);
45
64
  if (user) {
46
65
  req.user = user;
@@ -50,7 +69,7 @@ function createAuthMiddleware(authManager) {
50
69
  }
51
70
  }
52
71
 
53
- // If no user in session, try remember me token
72
+ // If no user in session or JWT, try remember me token
54
73
  if (!req.user && authManager.rememberTokens) {
55
74
  const user = await authManager.verifyRememberToken(req, res);
56
75
  if (user) {
@@ -96,6 +115,44 @@ function createAuthMiddleware(authManager) {
96
115
  };
97
116
  }
98
117
 
118
+ /**
119
+ * Require JWT middleware
120
+ * Enforces valid Bearer JWT token in Authorization header
121
+ * @param {Object} [options] - Verification options
122
+ */
123
+ function requireJwt(options = {}) {
124
+ return async (req, res, next) => {
125
+ const authHeader = req.headers?.authorization || req.headers?.Authorization;
126
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
127
+ return res.status(401).json({
128
+ error: 'Unauthorized',
129
+ message: 'Bearer token required',
130
+ });
131
+ }
132
+
133
+ const token = authHeader.substring(7).trim();
134
+ try {
135
+ const payload = authManager.verifyJwt(token, options);
136
+ req.token = token;
137
+ req.jwtPayload = payload;
138
+
139
+ if (payload.id) {
140
+ const user = await authManager.findUserById(payload.id);
141
+ req.user = user || payload;
142
+ } else {
143
+ req.user = payload;
144
+ }
145
+
146
+ next();
147
+ } catch (err) {
148
+ return res.status(401).json({
149
+ error: 'Unauthorized',
150
+ message: err.message || 'Invalid JWT token',
151
+ });
152
+ }
153
+ };
154
+ }
155
+
99
156
  /**
100
157
  * Require guest middleware
101
158
  * Redirects authenticated users away
@@ -192,6 +249,9 @@ function createAuthMiddleware(authManager) {
192
249
  case 'auth':
193
250
  return requireAuth({ api: args.includes('api') });
194
251
 
252
+ case 'jwt':
253
+ return requireJwt();
254
+
195
255
  case 'guest':
196
256
  return requireGuest();
197
257
 
@@ -216,6 +276,7 @@ function createAuthMiddleware(authManager) {
216
276
 
217
277
  // Guard middleware
218
278
  requireAuth,
279
+ requireJwt,
219
280
  requireGuest,
220
281
  requireCan,
221
282
  requireVerified,
@@ -225,6 +286,7 @@ function createAuthMiddleware(authManager) {
225
286
 
226
287
  // Aliases for route config (factories — file-router calls requireAuth(opts) / requireGuest(opts))
227
288
  auth: requireAuth,
289
+ jwt: requireJwt,
228
290
  guest: requireGuest,
229
291
  };
230
292
  }
package/index.d.ts CHANGED
@@ -567,6 +567,28 @@ export function swaggerPlugin(options?: Record<string, unknown>): WebspressoPlug
567
567
 
568
568
  export function healthCheckPlugin(options?: Record<string, unknown>): WebspressoPlugin;
569
569
 
570
+ export interface CorsPluginOptions {
571
+ origin?:
572
+ | string
573
+ | string[]
574
+ | RegExp
575
+ | ((
576
+ origin: string,
577
+ callback: (err: Error | null, allow?: boolean | string) => void
578
+ ) => void);
579
+ methods?: string | string[];
580
+ allowedHeaders?: string | string[];
581
+ exposedHeaders?: string | string[];
582
+ credentials?: boolean;
583
+ maxAge?: number;
584
+ preflightContinue?: boolean;
585
+ optionsSuccessStatus?: number;
586
+ routes?: string | string[] | boolean;
587
+ global?: boolean;
588
+ }
589
+
590
+ export function corsPlugin(options?: CorsPluginOptions): WebspressoPlugin;
591
+
570
592
  export interface RedirectRule {
571
593
  from: string | RegExp;
572
594
  to: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.83",
3
+ "version": "0.0.85",
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",
@@ -72,7 +72,7 @@
72
72
  "nodemailer": "^9.0.3",
73
73
  "nunjucks": "^3.2.4",
74
74
  "sanitize-html": "^2.17.3",
75
- "sharp": "^0.33.5",
75
+ "sharp": "^0.35.3",
76
76
  "swup": "^4.8.3",
77
77
  "yaml": "^2.8.3",
78
78
  "zod": "^3.23.0",
@@ -116,7 +116,7 @@
116
116
  "chokidar": "^3.5.3",
117
117
  "dotenv": "^16.3.1",
118
118
  "express-rate-limit": "^8.4.1",
119
- "release-it": "^19.0.0",
119
+ "release-it": "^20.2.1",
120
120
  "supertest": "^6.3.4",
121
121
  "typescript": "~5.6.3",
122
122
  "vitest": "^3.0.0"
@@ -161,6 +161,78 @@ function createApiHandlers(options) {
161
161
  }
162
162
  }
163
163
 
164
+ /**
165
+ * Update current user profile
166
+ */
167
+ async function updateProfileHandler(req, res) {
168
+ try {
169
+ if (!req.session || !req.session.adminUser) {
170
+ return res.status(401).json({ error: 'Not authenticated' });
171
+ }
172
+
173
+ const userId = req.session.adminUser.id;
174
+ const { name, email, currentPassword, newPassword } = req.body || {};
175
+
176
+ if (!AdminUserRepo) {
177
+ return res.status(500).json({ error: 'Admin user system not initialized' });
178
+ }
179
+
180
+ const user = await AdminUserRepo.findById(userId);
181
+ if (!user) {
182
+ return res.status(404).json({ error: 'User not found' });
183
+ }
184
+
185
+ const updateData = {};
186
+
187
+ if (name && typeof name === 'string' && name.trim()) {
188
+ updateData.name = name.trim();
189
+ }
190
+
191
+ if (email && typeof email === 'string' && email.trim() && email.trim() !== user.email) {
192
+ const existing = await AdminUserRepo.query().where('email', email.trim()).whereNot('id', userId).first();
193
+ if (existing) {
194
+ return res.status(400).json({ error: 'Email is already in use by another user' });
195
+ }
196
+ updateData.email = email.trim();
197
+ }
198
+
199
+ if (newPassword) {
200
+ if (!currentPassword) {
201
+ return res.status(400).json({ error: 'Current password is required to set a new password' });
202
+ }
203
+ if (!comparePassword) {
204
+ return res.status(500).json({ error: 'Password comparison helper not available' });
205
+ }
206
+ const isValid = await comparePassword(currentPassword, user.password);
207
+ if (!isValid) {
208
+ return res.status(400).json({ error: 'Current password is incorrect' });
209
+ }
210
+ if (typeof newPassword !== 'string' || newPassword.length < 6) {
211
+ return res.status(400).json({ error: 'New password must be at least 6 characters long' });
212
+ }
213
+ updateData.password = await hashPassword(newPassword, 10);
214
+ }
215
+
216
+ if (Object.keys(updateData).length > 0) {
217
+ await AdminUserRepo.update(userId, updateData);
218
+ }
219
+
220
+ const updatedUser = await AdminUserRepo.findById(userId);
221
+ const sanitized = sanitizeForOutput(updatedUser, getModelFromDb('AdminUser') || { hidden: ['password'] });
222
+ if (sanitized) delete sanitized.password;
223
+
224
+ req.session.adminUser = {
225
+ ...req.session.adminUser,
226
+ name: updatedUser.name,
227
+ email: updatedUser.email,
228
+ };
229
+
230
+ res.json({ success: true, user: sanitized });
231
+ } catch (error) {
232
+ res.status(500).json({ error: error.message });
233
+ }
234
+ }
235
+
164
236
  /**
165
237
  * Get all models (admin enabled)
166
238
  */
@@ -680,6 +752,7 @@ function createApiHandlers(options) {
680
752
  loginHandler,
681
753
  logoutHandler,
682
754
  meHandler,
755
+ updateProfileHandler,
683
756
  modelsHandler,
684
757
  modelHandler,
685
758
  recordsListHandler,
@@ -161,6 +161,16 @@ var routes = {
161
161
  return SettingsPage;
162
162
  }
163
163
  },
164
+ '/profile': {
165
+ onmatch: async () => {
166
+ const isAuth = await checkAuth();
167
+ if (!isAuth) {
168
+ redirectToLogin();
169
+ return;
170
+ }
171
+ return ProfilePage;
172
+ }
173
+ },
164
174
  '/users/sessions': {
165
175
  onmatch: async () => {
166
176
  if (!(await guardUserManagementRoutes())) return;
@@ -274,20 +284,28 @@ function wrapCustomPageWithLayout(page, rawComp) {
274
284
  var config = window.__ADMIN_CONFIG__;
275
285
  if (config && config.pages) {
276
286
  config.pages.forEach(function(page) {
277
- if (!routes[page.path]) {
278
- routes[page.path] = {
279
- onmatch: async () => {
280
- const isAuth = await checkAuth();
281
- if (!isAuth) {
282
- redirectToLogin();
283
- return;
284
- }
285
- if (window.__customPages && window.__customPages[page.id]) {
286
- return wrapCustomPageWithLayout(page, window.__customPages[page.id]);
287
- }
288
- return createCustomPage(page);
287
+ var normPath = page.path ? (page.path.startsWith('/') ? page.path : '/' + page.path) : '/';
288
+ var routeHandler = {
289
+ onmatch: async () => {
290
+ const isAuth = await checkAuth();
291
+ if (!isAuth) {
292
+ redirectToLogin();
293
+ return;
289
294
  }
290
- };
295
+ const comp = (window.__customPages && window.__customPages[page.id])
296
+ ? wrapCustomPageWithLayout(page, window.__customPages[page.id])
297
+ : createCustomPage(page);
298
+ setTimeout(function() { m.redraw(); }, 10);
299
+ setTimeout(function() { m.redraw(); }, 100);
300
+ return comp;
301
+ }
302
+ };
303
+
304
+ if (!routes[normPath]) {
305
+ routes[normPath] = routeHandler;
306
+ }
307
+ if (normPath !== '/' && !routes[normPath + '/']) {
308
+ routes[normPath + '/'] = routeHandler;
291
309
  }
292
310
  });
293
311
  }
@@ -41,6 +41,7 @@ function loadPagesFromDir(dirPath) {
41
41
 
42
42
  let pageConfig = null;
43
43
  let componentFile = null;
44
+ let htmlFile = null;
44
45
 
45
46
  if (entry.isDirectory()) {
46
47
  const folderPath = path.join(resolvedDir, entry.name);
@@ -63,13 +64,34 @@ function loadPagesFromDir(dirPath) {
63
64
  break;
64
65
  }
65
66
  }
67
+
68
+ if (!componentFile) {
69
+ const htmlCandidates = ['index.html', 'page.html', 'component.html', `${entry.name}.html`];
70
+ for (const cand of htmlCandidates) {
71
+ const fullCandPath = path.join(folderPath, cand);
72
+ if (fs.existsSync(fullCandPath)) {
73
+ htmlFile = fullCandPath;
74
+ break;
75
+ }
76
+ }
77
+ }
66
78
  } else if (entry.isFile() && entry.name.endsWith('.json')) {
67
79
  const jsonPath = path.join(resolvedDir, entry.name);
68
80
  pageConfig = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
69
81
  const baseName = entry.name.slice(0, -5);
70
82
  const jsCandidate = path.join(resolvedDir, baseName + '.js');
83
+ const htmlCandidate = path.join(resolvedDir, baseName + '.html');
71
84
  if (fs.existsSync(jsCandidate)) {
72
85
  componentFile = jsCandidate;
86
+ } else if (fs.existsSync(htmlCandidate)) {
87
+ htmlFile = htmlCandidate;
88
+ }
89
+ } else if (entry.isFile() && entry.name.endsWith('.html')) {
90
+ const baseName = entry.name.slice(0, -5);
91
+ const jsonCandidate = path.join(resolvedDir, baseName + '.json');
92
+ if (!fs.existsSync(jsonCandidate)) {
93
+ pageConfig = { id: baseName, title: baseName, path: '/' + baseName };
94
+ htmlFile = path.join(resolvedDir, entry.name);
73
95
  }
74
96
  }
75
97
 
@@ -77,6 +99,9 @@ function loadPagesFromDir(dirPath) {
77
99
  if (componentFile && !pageConfig.componentFile) {
78
100
  pageConfig.componentFile = componentFile;
79
101
  }
102
+ if (htmlFile && !pageConfig.htmlFile && !pageConfig.componentFile) {
103
+ pageConfig.htmlFile = htmlFile;
104
+ }
80
105
  if (pageConfig.menu) {
81
106
  if (pageConfig.menu === true) {
82
107
  menuItems.push({
@@ -129,6 +154,24 @@ function registerModule(config, deps) {
129
154
  registerMenuGroups(config.menuGroups, registry);
130
155
  }
131
156
 
157
+ if (config.scripts) {
158
+ if (!Array.isArray(config.scripts)) {
159
+ throw new Error(`Module "${config.id}": scripts must be an array`);
160
+ }
161
+ for (const s of config.scripts) {
162
+ registry.registerScript(s);
163
+ }
164
+ }
165
+
166
+ if (config.styles) {
167
+ if (!Array.isArray(config.styles)) {
168
+ throw new Error(`Module "${config.id}": styles must be an array`);
169
+ }
170
+ for (const s of config.styles) {
171
+ registry.registerStyle(s);
172
+ }
173
+ }
174
+
132
175
  if (config.pages !== undefined) {
133
176
  if (!Array.isArray(config.pages)) {
134
177
  throw new Error(`Module "${config.id}": pages must be an array`);
@@ -150,6 +193,27 @@ function registerModule(config, deps) {
150
193
  menuList = menuList.concat(dirMenuItems);
151
194
  }
152
195
 
196
+ for (const p of pagesList) {
197
+ if (p.menu && !menuList.some(m => m.id === p.id)) {
198
+ if (p.menu === true) {
199
+ menuList.push({
200
+ id: p.id,
201
+ label: p.title,
202
+ path: p.path,
203
+ icon: p.icon,
204
+ });
205
+ } else if (typeof p.menu === 'object') {
206
+ menuList.push({
207
+ id: p.id,
208
+ label: p.title,
209
+ path: p.path,
210
+ icon: p.icon,
211
+ ...p.menu,
212
+ });
213
+ }
214
+ }
215
+ }
216
+
153
217
  if (pagesList.length > 0) {
154
218
  registerPages(config.id, pagesList, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
155
219
  }
@@ -192,8 +256,13 @@ function registerPages(moduleId, pages, deps) {
192
256
  for (const page of pages) {
193
257
  const pageId = page.id || moduleId;
194
258
 
195
- if (!page.title || !page.path) {
196
- throw new Error(`Module "${moduleId}", page "${pageId}": requires title and path`);
259
+ let htmlContent = page.html || null;
260
+ if (page.htmlFile) {
261
+ const resolvedHtmlPath = path.resolve(page.htmlFile);
262
+ if (!fs.existsSync(resolvedHtmlPath)) {
263
+ throw new Error(`Module "${moduleId}", page "${pageId}": htmlFile not found at ${page.htmlFile}`);
264
+ }
265
+ htmlContent = fs.readFileSync(resolvedHtmlPath, 'utf8');
197
266
  }
198
267
 
199
268
  registry.registerPage(pageId, {
@@ -203,6 +272,8 @@ function registerPages(moduleId, pages, deps) {
203
272
  description: page.description,
204
273
  permission: page.permission,
205
274
  layout: page.layout !== undefined ? page.layout : true,
275
+ url: page.url || page.iframeUrl || undefined,
276
+ html: htmlContent || undefined,
206
277
  });
207
278
 
208
279
  if (page.componentFile) {
@@ -218,7 +289,8 @@ function registerPages(moduleId, pages, deps) {
218
289
  registry.registerClientComponent(pageId, formattedCode);
219
290
  }
220
291
 
221
- ctx.addRoute('get', `${adminPath}${page.path}`, optionalAuth, serveAdminPanel);
292
+ const normPath = page.path.startsWith('/') ? page.path : '/' + page.path;
293
+ ctx.addRoute('get', `${adminPath}${normPath}`, optionalAuth, serveAdminPanel);
222
294
  }
223
295
  }
224
296
 
@@ -46,6 +46,10 @@ class AdminRegistry {
46
46
 
47
47
  // Client components: JS code strings for custom page rendering
48
48
  this.clientComponents = new Map();
49
+
50
+ // Custom scripts & styles for Admin Panel SSR rendering
51
+ this.scripts = [];
52
+ this.styles = [];
49
53
 
50
54
  // Hooks: lifecycle hooks
51
55
  this.hooks = {
@@ -266,6 +270,66 @@ class AdminRegistry {
266
270
  return this;
267
271
  }
268
272
 
273
+ /**
274
+ * Register a custom script URL or definition
275
+ * @param {string|Object} script - Script URL string or config object { src, code, async, defer, type, position }
276
+ */
277
+ registerScript(script) {
278
+ if (!script) return this;
279
+ if (typeof script === 'string') {
280
+ this.scripts.push({ src: script, position: 'head' });
281
+ } else if (typeof script === 'object') {
282
+ this.scripts.push({ position: 'head', ...script });
283
+ }
284
+ return this;
285
+ }
286
+
287
+ /**
288
+ * Register a custom style URL or CSS definition
289
+ * @param {string|Object} style - Style URL string or config object { href, code, rel }
290
+ */
291
+ registerStyle(style) {
292
+ if (!style) return this;
293
+ if (typeof style === 'string') {
294
+ this.styles.push({ href: style, rel: 'stylesheet' });
295
+ } else if (typeof style === 'object') {
296
+ this.styles.push({ rel: 'stylesheet', ...style });
297
+ }
298
+ return this;
299
+ }
300
+
301
+ /**
302
+ * Get formatted HTML tags for registered styles
303
+ */
304
+ getStylesHtml() {
305
+ return this.styles.map(s => {
306
+ if (s.code) return `<style>${s.code}</style>`;
307
+ if (s.href) return `<link rel="${s.rel || 'stylesheet'}" href="${s.href}">`;
308
+ return '';
309
+ }).filter(Boolean).join('\n');
310
+ }
311
+
312
+ /**
313
+ * Get formatted HTML tags for registered scripts
314
+ * @param {string} [position] - Filter by position ('head' or 'body')
315
+ */
316
+ getScriptsHtml(position) {
317
+ return this.scripts
318
+ .filter(s => !position || s.position === position)
319
+ .map(s => {
320
+ if (s.code) return `<script>${s.code}</script>`;
321
+ if (s.src) {
322
+ const asyncAttr = s.async ? ' async' : '';
323
+ const deferAttr = s.defer ? ' defer' : '';
324
+ const typeAttr = s.type ? ` type="${s.type}"` : '';
325
+ return `<script src="${s.src}"${asyncAttr}${deferAttr}${typeAttr}></script>`;
326
+ }
327
+ return '';
328
+ })
329
+ .filter(Boolean)
330
+ .join('\n');
331
+ }
332
+
269
333
  /**
270
334
  * Register a client-side component for a custom page
271
335
  * @param {string} pageId - Page ID (must match a registered page)
@@ -415,6 +479,8 @@ class AdminRegistry {
415
479
  description: p.description,
416
480
  permission: p.permission,
417
481
  layout: p.layout !== undefined ? p.layout : true,
482
+ url: p.url || p.iframeUrl,
483
+ html: p.html,
418
484
  hasClientComponent: this.clientComponents.has(p.id),
419
485
  })),
420
486
 
@@ -460,6 +526,8 @@ class AdminRegistry {
460
526
  this.menuGroups.clear();
461
527
  this.fieldRenderers.clear();
462
528
  this.clientComponents.clear();
529
+ this.scripts = [];
530
+ this.styles = [];
463
531
  Object.keys(this.hooks).forEach(k => this.hooks[k] = []);
464
532
  }
465
533
  }
@@ -15,6 +15,7 @@ const { registerDashboardWidgets, generateDashboardComponent } = require('./modu
15
15
  const { registerDefaultBulkActions, generateBulkActionsComponent } = require('./modules/bulk-actions');
16
16
  const { registerDefaultPages, createCustomPageApiHandlers, generateCustomPageComponent } = require('./modules/custom-pages');
17
17
  const { registerModelMenuItems, registerSystemMenuItems, generateMenuComponent } = require('./modules/menu');
18
+ const { generateProfileComponent } = require('./modules/profile');
18
19
  const { registerModule, registerPageDir } = require('./core/admin-module');
19
20
 
20
21
 
@@ -70,10 +71,15 @@ function adminPanelPlugin(options = {}) {
70
71
  version: '2.0.0',
71
72
  description: 'Modular admin panel for Webspresso with extensions support',
72
73
 
73
- // CSP requirements for admin panel scripts
74
+ // CSP requirements for admin panel scripts, styles, and custom page iframe sources
74
75
  // Note: cdn.quilljs.com 301-redirects to cdn.jsdelivr.net; CSP is enforced on the final URL.
75
76
  csp: {
76
- styleSrc: ['https://cdn.quilljs.com', 'https://cdn.jsdelivr.net'],
77
+ styleSrc: [
78
+ 'https://cdn.quilljs.com',
79
+ 'https://cdn.jsdelivr.net',
80
+ 'https://unpkg.com',
81
+ 'https://cdn.tailwindcss.com',
82
+ ],
77
83
  scriptSrc: [
78
84
  'https://cdn.quilljs.com',
79
85
  'https://cdn.jsdelivr.net',
@@ -81,6 +87,7 @@ function adminPanelPlugin(options = {}) {
81
87
  'https://cdn.tailwindcss.com',
82
88
  ],
83
89
  connectSrc: ['https://unpkg.com', 'https://cdn.tailwindcss.com', 'https://cdn.jsdelivr.net'],
90
+ frameSrc: ["'self'", 'data:', 'blob:', 'https:'],
84
91
  },
85
92
  enabled,
86
93
  registry, // Expose registry for external configuration
@@ -120,6 +127,12 @@ function adminPanelPlugin(options = {}) {
120
127
  serveAdminPanel,
121
128
  });
122
129
  },
130
+ registerScript(script) {
131
+ return registry.registerScript(script);
132
+ },
133
+ registerStyle(style) {
134
+ return registry.registerStyle(style);
135
+ },
123
136
 
124
137
  _ctx: null,
125
138
  },
@@ -252,6 +265,7 @@ function adminPanelPlugin(options = {}) {
252
265
  ctx.addRoute('post', `${adminPath}/api/auth/login`, apiHandlers.loginHandler);
253
266
  ctx.addRoute('post', `${adminPath}/api/auth/logout`, requireAuth, apiHandlers.logoutHandler);
254
267
  ctx.addRoute('get', `${adminPath}/api/auth/me`, requireAuth, apiHandlers.meHandler);
268
+ ctx.addRoute('put', `${adminPath}/api/auth/profile`, requireAuth, apiHandlers.updateProfileHandler);
255
269
 
256
270
  // Extensions API routes
257
271
  ctx.addRoute('get', `${adminPath}/api/extensions/config`, requireAuth, extensionHandlers.configHandler);
@@ -273,7 +287,7 @@ function adminPanelPlugin(options = {}) {
273
287
 
274
288
  // Custom pages API routes
275
289
  ctx.addRoute('get', `${adminPath}/api/extensions/pages/:pageId/data`, requireAuth, pageHandlers.getPageData);
276
- ctx.addRoute('post', `${adminPath}/api/extensions/pages/:pageId/actions/:actionId`, requireAuth, pageHandlers.executePageAction);
290
+ ctx.addRoute('all', `${adminPath}/api/extensions/pages/:pageId/actions/:actionId`, requireAuth, pageHandlers.executePageAction);
277
291
 
278
292
  // User management API routes
279
293
  if (userHandlers) {
@@ -376,6 +390,7 @@ function generateAdminPanelHtml(adminPath, registry) {
376
390
  const dashboardComponent = generateDashboardComponent();
377
391
  const bulkActionsComponent = generateBulkActionsComponent();
378
392
  const customPageComponent = generateCustomPageComponent();
393
+ const profileComponent = generateProfileComponent();
379
394
 
380
395
  const settings = registry.settings;
381
396
  const fieldRenderersCode = Array.from(registry.fieldRenderers.entries()).map(([type, r]) => {
@@ -431,12 +446,14 @@ function generateAdminPanelHtml(adminPath, registry) {
431
446
  .dark .ql-snow .ql-fill { fill: #94a3b8; }
432
447
  .dark .ql-picker { color: #cbd5e1; }
433
448
  </style>
449
+ ${registry.getStylesHtml()}
450
+ ${registry.getScriptsHtml('head')}
434
451
  </head>
435
452
  <body class="min-h-screen bg-gray-50 dark:bg-slate-950 text-gray-900 dark:text-slate-100 antialiased">
436
453
  <div id="app"></div>
437
454
  <script>
438
455
  window.__ADMIN_PATH__ = ${JSON.stringify(adminPath)};
439
- window.__ADMIN_CONFIG__ = ${JSON.stringify(registry.toClientConfig())};
456
+ window.__ADMIN_CONFIG__ = ${JSON.stringify(registry.toClientConfig()).replace(/</g, '\\u003c')};
440
457
  </script>
441
458
  <script>
442
459
  // Helper functions
@@ -455,6 +472,7 @@ function generateAdminPanelHtml(adminPath, registry) {
455
472
  ${dashboardComponent}
456
473
  ${bulkActionsComponent}
457
474
  ${customPageComponent}
475
+ ${profileComponent}
458
476
 
459
477
  // Custom page components container
460
478
  window.__customPages = window.__customPages || {};
@@ -465,6 +483,7 @@ function generateAdminPanelHtml(adminPath, registry) {
465
483
  ${fieldRenderersCode}
466
484
  </script>
467
485
  <script>${appScript}</script>
486
+ ${registry.getScriptsHtml('body')}
468
487
  </body>
469
488
  </html>`;
470
489
  }