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.
- package/README.md +4 -7
- package/bin/commands/add-tailwind.js +151 -0
- package/bin/commands/api.js +70 -0
- package/bin/commands/db-make.js +76 -0
- package/bin/commands/db-migrate.js +43 -0
- package/bin/commands/db-rollback.js +48 -0
- package/bin/commands/db-status.js +53 -0
- package/bin/commands/dev.js +73 -0
- package/bin/commands/new.js +634 -0
- package/bin/commands/page.js +134 -0
- package/bin/commands/seed.js +154 -0
- package/bin/commands/start.js +30 -0
- package/bin/utils/db.js +54 -0
- package/bin/utils/migration.js +36 -0
- package/bin/utils/project.js +97 -0
- package/bin/utils/seed.js +112 -0
- package/bin/webspresso.js +24 -1696
- package/core/orm/index.js +14 -1
- package/core/orm/migrations/scaffold.js +5 -0
- package/core/orm/model.js +8 -0
- package/core/orm/schema-helpers.js +39 -1
- package/core/orm/seeder.js +56 -3
- package/core/orm/types.js +28 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/plugins/admin-panel/admin-user-model.js +42 -0
- package/plugins/admin-panel/api.js +436 -0
- package/plugins/admin-panel/app.js +68 -0
- package/plugins/admin-panel/auth.js +157 -0
- package/plugins/admin-panel/components.js +359 -0
- package/plugins/admin-panel/field-renderers/array.js +57 -0
- package/plugins/admin-panel/field-renderers/basic.js +205 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +124 -0
- package/plugins/admin-panel/field-renderers/index.js +93 -0
- package/plugins/admin-panel/field-renderers/json.js +52 -0
- package/plugins/admin-panel/field-renderers/relations.js +96 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +83 -0
- package/plugins/admin-panel/index.js +187 -0
- package/plugins/admin-panel/migration-template.js +39 -0
- package/plugins/admin-panel/styles.js +9 -0
- 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
|
+
};
|