webspresso 0.0.13 → 0.0.14

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.
Files changed (41) hide show
  1. package/README.md +4 -7
  2. package/bin/commands/add-tailwind.js +151 -0
  3. package/bin/commands/api.js +70 -0
  4. package/bin/commands/db-make.js +76 -0
  5. package/bin/commands/db-migrate.js +43 -0
  6. package/bin/commands/db-rollback.js +48 -0
  7. package/bin/commands/db-status.js +53 -0
  8. package/bin/commands/dev.js +73 -0
  9. package/bin/commands/new.js +634 -0
  10. package/bin/commands/page.js +134 -0
  11. package/bin/commands/seed.js +154 -0
  12. package/bin/commands/start.js +30 -0
  13. package/bin/utils/db.js +54 -0
  14. package/bin/utils/migration.js +36 -0
  15. package/bin/utils/project.js +97 -0
  16. package/bin/utils/seed.js +112 -0
  17. package/bin/webspresso.js +24 -1696
  18. package/core/orm/index.js +14 -1
  19. package/core/orm/migrations/scaffold.js +5 -0
  20. package/core/orm/model.js +8 -0
  21. package/core/orm/schema-helpers.js +39 -1
  22. package/core/orm/seeder.js +56 -3
  23. package/core/orm/types.js +28 -1
  24. package/index.js +2 -1
  25. package/package.json +1 -1
  26. package/plugins/admin-panel/admin-user-model.js +42 -0
  27. package/plugins/admin-panel/api.js +436 -0
  28. package/plugins/admin-panel/app.js +68 -0
  29. package/plugins/admin-panel/auth.js +157 -0
  30. package/plugins/admin-panel/components.js +359 -0
  31. package/plugins/admin-panel/field-renderers/array.js +57 -0
  32. package/plugins/admin-panel/field-renderers/basic.js +205 -0
  33. package/plugins/admin-panel/field-renderers/file-upload.js +124 -0
  34. package/plugins/admin-panel/field-renderers/index.js +93 -0
  35. package/plugins/admin-panel/field-renderers/json.js +52 -0
  36. package/plugins/admin-panel/field-renderers/relations.js +96 -0
  37. package/plugins/admin-panel/field-renderers/rich-text.js +83 -0
  38. package/plugins/admin-panel/index.js +187 -0
  39. package/plugins/admin-panel/migration-template.js +39 -0
  40. package/plugins/admin-panel/styles.js +9 -0
  41. package/plugins/index.js +2 -0
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Admin Panel API Routes
3
+ * CRUD endpoints for admin panel
4
+ * @module plugins/admin-panel/api
5
+ */
6
+
7
+ const { getAllModels, getModel } = require('../../../core/orm/model');
8
+ const { checkAdminExists, setupAdmin, login, logout, requireAuth } = require('./auth');
9
+
10
+ /**
11
+ * Create API route handlers
12
+ * @param {Object} options - Options
13
+ * @param {string} options.path - Admin panel path
14
+ * @param {Object} options.db - Database instance
15
+ * @param {Object} options.AdminUser - AdminUser model
16
+ * @param {Function} options.hashPassword - Bcrypt hash function
17
+ * @param {Function} options.comparePassword - Bcrypt compare function
18
+ * @returns {Object} Route handlers
19
+ */
20
+ function createApiHandlers(options) {
21
+ const { path, db, AdminUser, hashPassword, comparePassword } = options;
22
+ const adminPath = path || '/_admin';
23
+ const apiPath = `${adminPath}/api`;
24
+
25
+ // Get AdminUser repository
26
+ let AdminUserRepo = null;
27
+ if (db && AdminUser) {
28
+ AdminUserRepo = db.createRepository(AdminUser);
29
+ }
30
+
31
+ /**
32
+ * Check if admin user exists
33
+ */
34
+ async function checkHandler(req, res) {
35
+ try {
36
+ if (!AdminUserRepo) {
37
+ return res.json({ exists: false });
38
+ }
39
+ const exists = await checkAdminExists(AdminUserRepo);
40
+ res.json({ exists });
41
+ } catch (error) {
42
+ res.status(500).json({ error: error.message });
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Setup first admin user
48
+ */
49
+ async function setupHandler(req, res) {
50
+ try {
51
+ if (!AdminUserRepo || !hashPassword) {
52
+ return res.status(500).json({ error: 'Admin user system not initialized' });
53
+ }
54
+
55
+ const { email, password, name } = req.body;
56
+
57
+ if (!email || !password || !name) {
58
+ return res.status(400).json({ error: 'Email, password, and name are required' });
59
+ }
60
+
61
+ const admin = await setupAdmin(AdminUserRepo, { email, password, name }, hashPassword);
62
+
63
+ // Set session
64
+ req.session.adminUser = admin;
65
+
66
+ res.json({ success: true, user: admin });
67
+ } catch (error) {
68
+ res.status(400).json({ error: error.message });
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Login handler
74
+ */
75
+ async function loginHandler(req, res) {
76
+ try {
77
+ if (!AdminUserRepo || !comparePassword) {
78
+ return res.status(500).json({ error: 'Admin user system not initialized' });
79
+ }
80
+
81
+ const { email, password } = req.body;
82
+
83
+ if (!email || !password) {
84
+ return res.status(400).json({ error: 'Email and password are required' });
85
+ }
86
+
87
+ const user = await login(AdminUserRepo, email, password, comparePassword);
88
+
89
+ if (!user) {
90
+ return res.status(401).json({ error: 'Invalid credentials' });
91
+ }
92
+
93
+ // Set session
94
+ req.session.adminUser = user;
95
+
96
+ res.json({ success: true, user });
97
+ } catch (error) {
98
+ res.status(500).json({ error: error.message });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Logout handler
104
+ */
105
+ async function logoutHandler(req, res) {
106
+ try {
107
+ await logout(req, res);
108
+ res.json({ success: true });
109
+ } catch (error) {
110
+ res.status(500).json({ error: error.message });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get current user
116
+ */
117
+ function meHandler(req, res) {
118
+ if (req.session && req.session.adminUser) {
119
+ res.json({ user: req.session.adminUser });
120
+ } else {
121
+ res.status(401).json({ error: 'Not authenticated' });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get all models (admin enabled)
127
+ */
128
+ function modelsHandler(req, res) {
129
+ try {
130
+ const allModels = getAllModels();
131
+ const adminModels = [];
132
+
133
+ for (const [name, model] of allModels) {
134
+ if (model.admin && model.admin.enabled) {
135
+ adminModels.push({
136
+ name: model.name,
137
+ table: model.table,
138
+ label: model.admin.label || model.name,
139
+ icon: model.admin.icon || null,
140
+ primaryKey: model.primaryKey,
141
+ columns: Array.from(model.columns.keys()),
142
+ relations: Object.keys(model.relations),
143
+ });
144
+ }
145
+ }
146
+
147
+ res.json({ models: adminModels });
148
+ } catch (error) {
149
+ res.status(500).json({ error: error.message });
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get model metadata
155
+ */
156
+ function modelHandler(req, res) {
157
+ try {
158
+ const { model: modelName } = req.params;
159
+ const model = getModel(modelName);
160
+
161
+ if (!model) {
162
+ return res.status(404).json({ error: 'Model not found' });
163
+ }
164
+
165
+ if (!model.admin || !model.admin.enabled) {
166
+ return res.status(403).json({ error: 'Model not enabled in admin panel' });
167
+ }
168
+
169
+ // Build column metadata
170
+ const columns = [];
171
+ for (const [name, meta] of model.columns) {
172
+ columns.push({
173
+ name,
174
+ type: meta.type,
175
+ nullable: meta.nullable || false,
176
+ default: meta.default,
177
+ maxLength: meta.maxLength,
178
+ enumValues: meta.enumValues,
179
+ references: meta.references,
180
+ customField: model.admin.customFields?.[name] || null,
181
+ });
182
+ }
183
+
184
+ res.json({
185
+ name: model.name,
186
+ table: model.table,
187
+ label: model.admin.label || model.name,
188
+ icon: model.admin.icon || null,
189
+ primaryKey: model.primaryKey,
190
+ columns,
191
+ relations: Object.keys(model.relations),
192
+ queries: Object.keys(model.admin.queries || {}),
193
+ });
194
+ } catch (error) {
195
+ res.status(500).json({ error: error.message });
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Get records (paginated)
201
+ */
202
+ async function recordsListHandler(req, res) {
203
+ try {
204
+ const { model: modelName } = req.params;
205
+ const model = getModel(modelName);
206
+
207
+ if (!model || !model.admin || !model.admin.enabled) {
208
+ return res.status(404).json({ error: 'Model not found or not enabled' });
209
+ }
210
+
211
+ const repo = db.createRepository(model);
212
+ const page = parseInt(req.query.page) || 1;
213
+ const perPage = parseInt(req.query.perPage) || 15;
214
+ const offset = (page - 1) * perPage;
215
+
216
+ // Build query
217
+ let query = repo.query();
218
+
219
+ // Apply filters if provided
220
+ if (req.query.search) {
221
+ // Simple search on string columns
222
+ const searchTerm = `%${req.query.search}%`;
223
+ const stringColumns = Array.from(model.columns.entries())
224
+ .filter(([_, meta]) => meta.type === 'string' || meta.type === 'text')
225
+ .map(([name]) => name);
226
+
227
+ if (stringColumns.length > 0) {
228
+ query = query.where(function() {
229
+ for (let i = 0; i < stringColumns.length; i++) {
230
+ if (i === 0) {
231
+ this.where(stringColumns[i], 'like', searchTerm);
232
+ } else {
233
+ this.orWhere(stringColumns[i], 'like', searchTerm);
234
+ }
235
+ }
236
+ });
237
+ }
238
+ }
239
+
240
+ // Get total count
241
+ const countConditions = {};
242
+ if (req.query.search) {
243
+ // For count, we'll use a simpler approach
244
+ // In a real implementation, you'd want to match the same search logic
245
+ }
246
+ const total = await repo.count(countConditions);
247
+
248
+ // Get records
249
+ const records = await query.list();
250
+
251
+ res.json({
252
+ data: records,
253
+ pagination: {
254
+ page,
255
+ perPage,
256
+ total,
257
+ totalPages: Math.ceil(total / perPage),
258
+ },
259
+ });
260
+ } catch (error) {
261
+ res.status(500).json({ error: error.message });
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Get single record
267
+ */
268
+ async function recordHandler(req, res) {
269
+ try {
270
+ const { model: modelName, id } = req.params;
271
+ const model = getModel(modelName);
272
+
273
+ if (!model || !model.admin || !model.admin.enabled) {
274
+ return res.status(404).json({ error: 'Model not found or not enabled' });
275
+ }
276
+
277
+ const repo = db.createRepository(model);
278
+ const record = await repo.findById(id);
279
+
280
+ if (!record) {
281
+ return res.status(404).json({ error: 'Record not found' });
282
+ }
283
+
284
+ res.json({ data: record });
285
+ } catch (error) {
286
+ res.status(500).json({ error: error.message });
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Create record
292
+ */
293
+ async function createRecordHandler(req, res) {
294
+ try {
295
+ const { model: modelName } = req.params;
296
+ const model = getModel(modelName);
297
+
298
+ if (!model || !model.admin || !model.admin.enabled) {
299
+ return res.status(404).json({ error: 'Model not found or not enabled' });
300
+ }
301
+
302
+ const repo = db.createRepository(model);
303
+ const record = await repo.create(req.body);
304
+
305
+ res.status(201).json({ data: record });
306
+ } catch (error) {
307
+ res.status(400).json({ error: error.message });
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Update record
313
+ */
314
+ async function updateRecordHandler(req, res) {
315
+ try {
316
+ const { model: modelName, id } = req.params;
317
+ const model = getModel(modelName);
318
+
319
+ if (!model || !model.admin || !model.admin.enabled) {
320
+ return res.status(404).json({ error: 'Model not found or not enabled' });
321
+ }
322
+
323
+ const repo = db.createRepository(model);
324
+ const record = await repo.update(id, req.body);
325
+
326
+ if (!record) {
327
+ return res.status(404).json({ error: 'Record not found' });
328
+ }
329
+
330
+ res.json({ data: record });
331
+ } catch (error) {
332
+ res.status(400).json({ error: error.message });
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Delete record
338
+ */
339
+ async function deleteRecordHandler(req, res) {
340
+ try {
341
+ const { model: modelName, id } = req.params;
342
+ const model = getModel(modelName);
343
+
344
+ if (!model || !model.admin || !model.admin.enabled) {
345
+ return res.status(404).json({ error: 'Model not found or not enabled' });
346
+ }
347
+
348
+ const repo = db.createRepository(model);
349
+ const deleted = await repo.delete(id);
350
+
351
+ if (!deleted) {
352
+ return res.status(404).json({ error: 'Record not found' });
353
+ }
354
+
355
+ res.json({ success: true });
356
+ } catch (error) {
357
+ res.status(500).json({ error: error.message });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Get relation data
363
+ */
364
+ async function relationHandler(req, res) {
365
+ try {
366
+ const { model: modelName, relation: relationName } = req.params;
367
+ const model = getModel(modelName);
368
+
369
+ if (!model || !model.admin || !model.admin.enabled) {
370
+ return res.status(404).json({ error: 'Model not found or not enabled' });
371
+ }
372
+
373
+ const relation = model.relations[relationName];
374
+ if (!relation) {
375
+ return res.status(404).json({ error: 'Relation not found' });
376
+ }
377
+
378
+ const relatedModel = relation.model();
379
+ const relatedRepo = db.createRepository(relatedModel);
380
+
381
+ // Get all related records (for dropdown/select)
382
+ const records = await relatedRepo.findAll();
383
+
384
+ res.json({ data: records });
385
+ } catch (error) {
386
+ res.status(500).json({ error: error.message });
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Execute custom query
392
+ */
393
+ async function queryHandler(req, res) {
394
+ try {
395
+ const { model: modelName, query: queryName } = req.params;
396
+ const model = getModel(modelName);
397
+
398
+ if (!model || !model.admin || !model.admin.enabled) {
399
+ return res.status(404).json({ error: 'Model not found or not enabled' });
400
+ }
401
+
402
+ const queryFn = model.admin.queries?.[queryName];
403
+ if (!queryFn || typeof queryFn !== 'function') {
404
+ return res.status(404).json({ error: 'Query not found' });
405
+ }
406
+
407
+ const repo = db.createRepository(model);
408
+ const result = await queryFn(repo);
409
+
410
+ res.json({ data: result });
411
+ } catch (error) {
412
+ res.status(500).json({ error: error.message });
413
+ }
414
+ }
415
+
416
+ return {
417
+ checkHandler,
418
+ setupHandler,
419
+ loginHandler,
420
+ logoutHandler,
421
+ meHandler,
422
+ modelsHandler,
423
+ modelHandler,
424
+ recordsListHandler,
425
+ recordHandler,
426
+ createRecordHandler,
427
+ updateRecordHandler,
428
+ deleteRecordHandler,
429
+ relationHandler,
430
+ queryHandler,
431
+ };
432
+ }
433
+
434
+ module.exports = {
435
+ createApiHandlers,
436
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Admin Panel Mithril.js Application
3
+ * Main SPA entry point
4
+ */
5
+
6
+ const components = require('./components');
7
+
8
+ module.exports = components + `
9
+ // Router
10
+ m.route(document.getElementById('app'), '/', {
11
+ '/': {
12
+ onmatch: async () => {
13
+ // Check if user is authenticated
14
+ try {
15
+ const result = await api.get('/auth/me');
16
+ state.user = result.user;
17
+ } catch (err) {
18
+ state.user = null;
19
+ }
20
+
21
+ // Check if setup is needed
22
+ try {
23
+ const checkResult = await api.get('/auth/check');
24
+ state.needsSetup = !checkResult.exists;
25
+ } catch (err) {
26
+ state.needsSetup = false;
27
+ }
28
+
29
+ if (state.needsSetup) {
30
+ return SetupForm;
31
+ }
32
+ if (!state.user) {
33
+ return LoginForm;
34
+ }
35
+ return ModelList;
36
+ }
37
+ },
38
+ '/login': LoginForm,
39
+ '/setup': SetupForm,
40
+ '/models/:model': {
41
+ onmatch: async (params) => {
42
+ if (!state.user) {
43
+ m.route.set('/login');
44
+ return;
45
+ }
46
+ return RecordList;
47
+ }
48
+ },
49
+ '/models/:model/new': {
50
+ onmatch: async (params) => {
51
+ if (!state.user) {
52
+ m.route.set('/login');
53
+ return;
54
+ }
55
+ return RecordForm;
56
+ }
57
+ },
58
+ '/models/:model/edit/:id': {
59
+ onmatch: async (params) => {
60
+ if (!state.user) {
61
+ m.route.set('/login');
62
+ return;
63
+ }
64
+ return RecordForm;
65
+ }
66
+ },
67
+ });
68
+ `;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Authentication Middleware
3
+ * Session-based authentication for admin panel
4
+ * @module plugins/admin-panel/auth
5
+ */
6
+
7
+ /**
8
+ * Check if admin user exists in database
9
+ * @param {Object} adminUserRepo - AdminUser repository
10
+ * @returns {Promise<boolean>}
11
+ */
12
+ async function checkAdminExists(adminUserRepo) {
13
+ try {
14
+ const count = await adminUserRepo.count();
15
+ return count > 0;
16
+ } catch (error) {
17
+ // Table might not exist yet
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Create first admin user
24
+ * @param {Object} adminUserRepo - AdminUser repository
25
+ * @param {Object} data - User data { email, password, name }
26
+ * @param {Function} hashPassword - Password hashing function (bcrypt)
27
+ * @returns {Promise<Object>} Created admin user
28
+ */
29
+ async function setupAdmin(adminUserRepo, data, hashPassword) {
30
+ const { email, password, name } = data;
31
+
32
+ if (!email || !password || !name) {
33
+ throw new Error('Email, password, and name are required');
34
+ }
35
+
36
+ // Check if admin already exists
37
+ const exists = await checkAdminExists(adminUserRepo);
38
+ if (exists) {
39
+ throw new Error('Admin user already exists');
40
+ }
41
+
42
+ // Hash password
43
+ const hashedPassword = await hashPassword(password, 10);
44
+
45
+ // Create admin user
46
+ const admin = await adminUserRepo.create({
47
+ email,
48
+ password: hashedPassword,
49
+ name,
50
+ role: 'admin',
51
+ active: true,
52
+ });
53
+
54
+ // Remove password from response
55
+ delete admin.password;
56
+
57
+ return admin;
58
+ }
59
+
60
+ /**
61
+ * Verify password against hash
62
+ * @param {string} password - Plain password
63
+ * @param {string} hash - Hashed password
64
+ * @param {Function} compare - Password comparison function (bcrypt.compare)
65
+ * @returns {Promise<boolean>}
66
+ */
67
+ async function verifyPassword(password, hash, compare) {
68
+ return await compare(password, hash);
69
+ }
70
+
71
+ /**
72
+ * Login user and set session
73
+ * @param {Object} adminUserRepo - AdminUser repository
74
+ * @param {string} email - User email
75
+ * @param {string} password - User password
76
+ * @param {Function} compare - Password comparison function
77
+ * @returns {Promise<Object|null>} User object or null if invalid
78
+ */
79
+ async function login(adminUserRepo, email, password, compare) {
80
+ // Find user by email
81
+ const user = await adminUserRepo.query().where('email', email).first();
82
+
83
+ if (!user || !user.active) {
84
+ return null;
85
+ }
86
+
87
+ // Verify password
88
+ const isValid = await verifyPassword(password, user.password, compare);
89
+
90
+ if (!isValid) {
91
+ return null;
92
+ }
93
+
94
+ // Remove password from response
95
+ const { password: _, ...userWithoutPassword } = user;
96
+
97
+ return userWithoutPassword;
98
+ }
99
+
100
+ /**
101
+ * Logout user (clear session)
102
+ * @param {Object} req - Express request
103
+ * @param {Object} res - Express response
104
+ * @returns {Promise<void>}
105
+ */
106
+ async function logout(req, res) {
107
+ return new Promise((resolve, reject) => {
108
+ req.session.destroy((err) => {
109
+ if (err) {
110
+ reject(err);
111
+ } else {
112
+ resolve();
113
+ }
114
+ });
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Require authentication middleware
120
+ * @param {Object} req - Express request
121
+ * @param {Object} res - Express response
122
+ * @param {Function} next - Express next
123
+ */
124
+ function requireAuth(req, res, next) {
125
+ if (req.session && req.session.adminUser) {
126
+ return next();
127
+ }
128
+
129
+ // If it's an API request, return JSON error
130
+ if (req.path.startsWith('/api/')) {
131
+ return res.status(401).json({ error: 'Unauthorized' });
132
+ }
133
+
134
+ // Otherwise redirect to login (handled by frontend)
135
+ return res.status(401).json({ error: 'Unauthorized', redirect: '/_admin/login' });
136
+ }
137
+
138
+ /**
139
+ * Optional auth middleware (doesn't fail if not authenticated)
140
+ * @param {Object} req - Express request
141
+ * @param {Object} res - Express response
142
+ * @param {Function} next - Express next
143
+ */
144
+ function optionalAuth(req, res, next) {
145
+ // Just pass through, frontend will handle auth state
146
+ next();
147
+ }
148
+
149
+ module.exports = {
150
+ checkAdminExists,
151
+ setupAdmin,
152
+ verifyPassword,
153
+ login,
154
+ logout,
155
+ requireAuth,
156
+ optionalAuth,
157
+ };