archicore 0.2.0 → 0.2.2
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/dist/cli/commands/init.js +37 -14
- package/dist/cli/commands/interactive.js +174 -21
- package/dist/github/github-service.d.ts +5 -1
- package/dist/github/github-service.js +21 -3
- package/dist/orchestrator/index.js +19 -0
- package/dist/semantic-memory/embedding-service.d.ts +8 -1
- package/dist/semantic-memory/embedding-service.js +141 -47
- package/dist/server/index.js +163 -1
- package/dist/server/routes/admin.js +439 -1
- package/dist/server/routes/api.js +17 -2
- package/dist/server/routes/auth.js +46 -0
- package/dist/server/routes/developer.js +1 -1
- package/dist/server/routes/device-auth.js +10 -1
- package/dist/server/routes/github.js +17 -4
- package/dist/server/routes/report-issue.d.ts +7 -0
- package/dist/server/routes/report-issue.js +307 -0
- package/dist/server/services/audit-service.d.ts +88 -0
- package/dist/server/services/audit-service.js +380 -0
- package/dist/server/services/auth-service.d.ts +32 -5
- package/dist/server/services/auth-service.js +347 -54
- package/dist/server/services/cache.d.ts +77 -0
- package/dist/server/services/cache.js +245 -0
- package/dist/server/services/database.d.ts +43 -0
- package/dist/server/services/database.js +221 -0
- package/dist/server/services/encryption.d.ts +48 -0
- package/dist/server/services/encryption.js +148 -0
- package/package.json +17 -2
|
@@ -2,14 +2,68 @@
|
|
|
2
2
|
* Admin API Routes for ArchiCore
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express';
|
|
5
|
+
import rateLimit from 'express-rate-limit';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
5
8
|
import { AuthService } from '../services/auth-service.js';
|
|
9
|
+
import { auditService } from '../services/audit-service.js';
|
|
6
10
|
import { authMiddleware, adminMiddleware } from './auth.js';
|
|
7
11
|
import { Logger } from '../../utils/logger.js';
|
|
8
12
|
export const adminRouter = Router();
|
|
9
13
|
const authService = AuthService.getInstance();
|
|
10
|
-
//
|
|
14
|
+
// Stricter rate limiting for admin routes (30 requests per minute)
|
|
15
|
+
const adminRateLimiter = rateLimit({
|
|
16
|
+
windowMs: 60 * 1000, // 1 minute
|
|
17
|
+
max: 30, // 30 requests per minute
|
|
18
|
+
message: { error: 'Too many admin requests, please slow down', retryAfter: 60 },
|
|
19
|
+
standardHeaders: true,
|
|
20
|
+
legacyHeaders: false,
|
|
21
|
+
handler: (_req, res) => {
|
|
22
|
+
Logger.warn('Admin rate limit exceeded');
|
|
23
|
+
res.status(429).json({
|
|
24
|
+
error: 'Too many admin requests',
|
|
25
|
+
message: 'Please wait before making more admin API calls',
|
|
26
|
+
retryAfter: 60
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Settings file path (defined early for public endpoints)
|
|
31
|
+
const SETTINGS_FILE = path.join(process.cwd(), '.archicore', 'settings.json');
|
|
32
|
+
// Load settings helper (defined early for public endpoints)
|
|
33
|
+
function loadSettingsPublic() {
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
36
|
+
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
|
37
|
+
return JSON.parse(data);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
Logger.error('Failed to load settings:', error);
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
// ===== PUBLIC ENDPOINTS (no auth required) =====
|
|
46
|
+
/**
|
|
47
|
+
* GET /api/admin/maintenance-status
|
|
48
|
+
* Get maintenance status (public endpoint for maintenance page)
|
|
49
|
+
*/
|
|
50
|
+
adminRouter.get('/maintenance-status', (_req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const settings = loadSettingsPublic();
|
|
53
|
+
res.json({
|
|
54
|
+
enabled: settings.maintenance?.enabled || false,
|
|
55
|
+
message: settings.maintenance?.message || 'ArchiCore is currently undergoing maintenance.'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
res.json({ enabled: false, message: '' });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// ===== PROTECTED ENDPOINTS (auth required) =====
|
|
63
|
+
// All admin routes below require authentication, admin role, and rate limiting
|
|
11
64
|
adminRouter.use(authMiddleware);
|
|
12
65
|
adminRouter.use(adminMiddleware);
|
|
66
|
+
adminRouter.use(adminRateLimiter);
|
|
13
67
|
/**
|
|
14
68
|
* GET /api/admin/users
|
|
15
69
|
* Get all users
|
|
@@ -49,6 +103,8 @@ adminRouter.get('/users/:id', async (req, res) => {
|
|
|
49
103
|
* Update user's subscription tier
|
|
50
104
|
*/
|
|
51
105
|
adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
106
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
107
|
+
const userAgent = req.headers['user-agent'];
|
|
52
108
|
try {
|
|
53
109
|
const { id } = req.params;
|
|
54
110
|
const { tier } = req.body;
|
|
@@ -57,11 +113,28 @@ adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
|
57
113
|
res.status(400).json({ error: 'Invalid tier. Valid tiers: ' + validTiers.join(', ') });
|
|
58
114
|
return;
|
|
59
115
|
}
|
|
116
|
+
// Get old tier for audit
|
|
117
|
+
const oldUser = await authService.getUser(id);
|
|
118
|
+
const oldTier = oldUser?.tier;
|
|
60
119
|
const updated = await authService.updateUserTier(id, tier);
|
|
61
120
|
if (!updated) {
|
|
62
121
|
res.status(404).json({ error: 'User not found' });
|
|
63
122
|
return;
|
|
64
123
|
}
|
|
124
|
+
// Audit log
|
|
125
|
+
await auditService.log({
|
|
126
|
+
userId: req.user?.id,
|
|
127
|
+
username: req.user?.username,
|
|
128
|
+
action: 'admin.tier_change',
|
|
129
|
+
ip,
|
|
130
|
+
userAgent,
|
|
131
|
+
details: {
|
|
132
|
+
targetUserId: id,
|
|
133
|
+
targetUsername: oldUser?.username,
|
|
134
|
+
oldTier,
|
|
135
|
+
newTier: tier
|
|
136
|
+
}
|
|
137
|
+
});
|
|
65
138
|
res.json({ success: true });
|
|
66
139
|
}
|
|
67
140
|
catch (error) {
|
|
@@ -74,13 +147,30 @@ adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
|
74
147
|
* Delete user
|
|
75
148
|
*/
|
|
76
149
|
adminRouter.delete('/users/:id', async (req, res) => {
|
|
150
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
151
|
+
const userAgent = req.headers['user-agent'];
|
|
77
152
|
try {
|
|
78
153
|
const { id } = req.params;
|
|
154
|
+
// Get user info before deletion for audit
|
|
155
|
+
const userToDelete = await authService.getUser(id);
|
|
79
156
|
const deleted = await authService.deleteUser(id);
|
|
80
157
|
if (!deleted) {
|
|
81
158
|
res.status(400).json({ error: 'Cannot delete user (admin or not found)' });
|
|
82
159
|
return;
|
|
83
160
|
}
|
|
161
|
+
// Audit log
|
|
162
|
+
await auditService.log({
|
|
163
|
+
userId: req.user?.id,
|
|
164
|
+
username: req.user?.username,
|
|
165
|
+
action: 'admin.user_delete',
|
|
166
|
+
ip,
|
|
167
|
+
userAgent,
|
|
168
|
+
details: {
|
|
169
|
+
deletedUserId: id,
|
|
170
|
+
deletedUsername: userToDelete?.username,
|
|
171
|
+
deletedEmail: userToDelete?.email
|
|
172
|
+
}
|
|
173
|
+
});
|
|
84
174
|
res.json({ success: true });
|
|
85
175
|
}
|
|
86
176
|
catch (error) {
|
|
@@ -120,4 +210,352 @@ adminRouter.get('/stats', async (_req, res) => {
|
|
|
120
210
|
res.status(500).json({ error: 'Failed to get statistics' });
|
|
121
211
|
}
|
|
122
212
|
});
|
|
213
|
+
// ===== AUDIT LOGS =====
|
|
214
|
+
/**
|
|
215
|
+
* GET /api/admin/audit
|
|
216
|
+
* Get audit logs with filtering and pagination
|
|
217
|
+
*/
|
|
218
|
+
adminRouter.get('/audit', async (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
const { userId, action, severity, success, startDate, endDate, limit = '50', offset = '0' } = req.query;
|
|
221
|
+
// Log that admin is viewing audit logs
|
|
222
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
223
|
+
const userAgent = req.headers['user-agent'];
|
|
224
|
+
await auditService.log({
|
|
225
|
+
userId: req.user?.id,
|
|
226
|
+
username: req.user?.username,
|
|
227
|
+
action: 'admin.view_logs',
|
|
228
|
+
ip,
|
|
229
|
+
userAgent,
|
|
230
|
+
details: { filters: { userId, action, severity } }
|
|
231
|
+
});
|
|
232
|
+
const result = await auditService.query({
|
|
233
|
+
userId: userId,
|
|
234
|
+
action: action,
|
|
235
|
+
severity: severity,
|
|
236
|
+
success: success ? success === 'true' : undefined,
|
|
237
|
+
startDate: startDate ? new Date(startDate) : undefined,
|
|
238
|
+
endDate: endDate ? new Date(endDate) : undefined,
|
|
239
|
+
limit: parseInt(limit, 10),
|
|
240
|
+
offset: parseInt(offset, 10)
|
|
241
|
+
});
|
|
242
|
+
res.json(result);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
Logger.error('Failed to get audit logs:', error);
|
|
246
|
+
res.status(500).json({ error: 'Failed to get audit logs' });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
/**
|
|
250
|
+
* GET /api/admin/audit/stats
|
|
251
|
+
* Get audit statistics
|
|
252
|
+
*/
|
|
253
|
+
adminRouter.get('/audit/stats', async (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const days = parseInt(req.query.days || '7', 10);
|
|
256
|
+
const stats = await auditService.getStats(days);
|
|
257
|
+
res.json(stats);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
Logger.error('Failed to get audit stats:', error);
|
|
261
|
+
res.status(500).json({ error: 'Failed to get audit statistics' });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
/**
|
|
265
|
+
* GET /api/admin/audit/user/:userId
|
|
266
|
+
* Get audit logs for specific user
|
|
267
|
+
*/
|
|
268
|
+
adminRouter.get('/audit/user/:userId', async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
const { userId } = req.params;
|
|
271
|
+
const limit = parseInt(req.query.limit || '50', 10);
|
|
272
|
+
const logs = await auditService.getUserLogs(userId, limit);
|
|
273
|
+
res.json({ logs });
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
Logger.error('Failed to get user audit logs:', error);
|
|
277
|
+
res.status(500).json({ error: 'Failed to get user audit logs' });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
/**
|
|
281
|
+
* POST /api/admin/audit/cleanup
|
|
282
|
+
* Clean up old audit logs (retention policy)
|
|
283
|
+
*/
|
|
284
|
+
adminRouter.post('/audit/cleanup', async (req, res) => {
|
|
285
|
+
try {
|
|
286
|
+
const retentionDays = parseInt(req.body.retentionDays || '90', 10);
|
|
287
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
288
|
+
const userAgent = req.headers['user-agent'];
|
|
289
|
+
// Log cleanup action
|
|
290
|
+
await auditService.log({
|
|
291
|
+
userId: req.user?.id,
|
|
292
|
+
username: req.user?.username,
|
|
293
|
+
action: 'admin.view_logs',
|
|
294
|
+
ip,
|
|
295
|
+
userAgent,
|
|
296
|
+
details: { operation: 'cleanup', retentionDays }
|
|
297
|
+
});
|
|
298
|
+
const removed = await auditService.cleanup(retentionDays);
|
|
299
|
+
res.json({ success: true, removed });
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
Logger.error('Failed to cleanup audit logs:', error);
|
|
303
|
+
res.status(500).json({ error: 'Failed to cleanup audit logs' });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// ===== SETTINGS =====
|
|
307
|
+
// Default settings
|
|
308
|
+
const defaultSettings = {
|
|
309
|
+
siteName: 'ArchiCore',
|
|
310
|
+
siteDescription: 'AI-Powered Software Architecture Analysis',
|
|
311
|
+
defaultTier: 'free',
|
|
312
|
+
limits: { free: 10, team: 100, pro: 500 },
|
|
313
|
+
security: {
|
|
314
|
+
requireEmailVerification: false,
|
|
315
|
+
allowRegistration: true,
|
|
316
|
+
sessionTimeout: 24
|
|
317
|
+
},
|
|
318
|
+
maintenance: { enabled: false, message: 'ArchiCore is currently undergoing maintenance.' },
|
|
319
|
+
integrations: { githubAppId: '', gitlabAppId: '', slackWebhook: '' },
|
|
320
|
+
email: { smtpHost: '', smtpPort: 587, smtpUser: '', smtpPass: '', fromEmail: '' },
|
|
321
|
+
notifications: { newUsers: true, tierChanges: true, errors: false, adminEmail: '' },
|
|
322
|
+
features: { api: true, webhooks: true, export: true, blog: true, changelog: true },
|
|
323
|
+
data: { backupSchedule: 'weekly', retentionDays: 90 }
|
|
324
|
+
};
|
|
325
|
+
function loadSettings() {
|
|
326
|
+
try {
|
|
327
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
328
|
+
const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
|
|
329
|
+
return { ...defaultSettings, ...JSON.parse(data) };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
Logger.error('Failed to load settings:', error);
|
|
334
|
+
}
|
|
335
|
+
return defaultSettings;
|
|
336
|
+
}
|
|
337
|
+
function saveSettings(settings) {
|
|
338
|
+
try {
|
|
339
|
+
const dir = path.dirname(SETTINGS_FILE);
|
|
340
|
+
if (!fs.existsSync(dir)) {
|
|
341
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
342
|
+
}
|
|
343
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
Logger.error('Failed to save settings:', error);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* GET /api/admin/settings
|
|
353
|
+
* Get system settings
|
|
354
|
+
*/
|
|
355
|
+
adminRouter.get('/settings', async (_req, res) => {
|
|
356
|
+
try {
|
|
357
|
+
const settings = loadSettings();
|
|
358
|
+
// Don't send sensitive data like SMTP password
|
|
359
|
+
const sanitized = {
|
|
360
|
+
...settings,
|
|
361
|
+
email: { ...settings.email, smtpPass: settings.email.smtpPass ? '********' : '' }
|
|
362
|
+
};
|
|
363
|
+
res.json(sanitized);
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
Logger.error('Failed to get settings:', error);
|
|
367
|
+
res.status(500).json({ error: 'Failed to get settings' });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
/**
|
|
371
|
+
* POST /api/admin/settings
|
|
372
|
+
* Update system settings
|
|
373
|
+
*/
|
|
374
|
+
adminRouter.post('/settings', async (req, res) => {
|
|
375
|
+
try {
|
|
376
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
377
|
+
const userAgent = req.headers['user-agent'];
|
|
378
|
+
const currentSettings = loadSettings();
|
|
379
|
+
const newSettings = { ...currentSettings, ...req.body };
|
|
380
|
+
// Preserve existing password if not provided or masked
|
|
381
|
+
if (!newSettings.email?.smtpPass || newSettings.email.smtpPass === '********') {
|
|
382
|
+
newSettings.email.smtpPass = currentSettings.email.smtpPass;
|
|
383
|
+
}
|
|
384
|
+
if (saveSettings(newSettings)) {
|
|
385
|
+
// Audit log
|
|
386
|
+
await auditService.log({
|
|
387
|
+
userId: req.user?.id,
|
|
388
|
+
username: req.user?.username,
|
|
389
|
+
action: 'admin.view_logs',
|
|
390
|
+
ip,
|
|
391
|
+
userAgent,
|
|
392
|
+
details: { operation: 'settings_update', changedKeys: Object.keys(req.body) }
|
|
393
|
+
});
|
|
394
|
+
res.json({ success: true });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
res.status(500).json({ error: 'Failed to save settings' });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
Logger.error('Failed to save settings:', error);
|
|
402
|
+
res.status(500).json({ error: 'Failed to save settings' });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
/**
|
|
406
|
+
* POST /api/admin/test-email
|
|
407
|
+
* Test email configuration
|
|
408
|
+
*/
|
|
409
|
+
adminRouter.post('/test-email', async (req, res) => {
|
|
410
|
+
try {
|
|
411
|
+
// For now, just validate the settings are present
|
|
412
|
+
const { smtpHost, smtpPort, smtpUser, fromEmail } = req.body;
|
|
413
|
+
if (!smtpHost || !smtpUser || !fromEmail) {
|
|
414
|
+
res.status(400).json({ error: 'Missing required email settings' });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// TODO: Implement actual email sending with nodemailer
|
|
418
|
+
Logger.info('Test email requested', { smtpHost, smtpPort, fromEmail });
|
|
419
|
+
res.json({ success: true, message: 'Email configuration looks valid' });
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
Logger.error('Failed to test email:', error);
|
|
423
|
+
res.status(500).json({ error: 'Failed to test email configuration' });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
/**
|
|
427
|
+
* GET /api/admin/export/all
|
|
428
|
+
* Export all data
|
|
429
|
+
*/
|
|
430
|
+
adminRouter.get('/export/all', async (_req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
const users = await authService.getAllUsers();
|
|
433
|
+
const settings = loadSettings();
|
|
434
|
+
const auditLogs = await auditService.query({ limit: 1000, offset: 0 });
|
|
435
|
+
// Sanitize user data (remove passwords)
|
|
436
|
+
const sanitizedUsers = users.map(u => {
|
|
437
|
+
const { passwordHash, ...safe } = u;
|
|
438
|
+
return safe;
|
|
439
|
+
});
|
|
440
|
+
res.json({
|
|
441
|
+
exportedAt: new Date().toISOString(),
|
|
442
|
+
users: sanitizedUsers,
|
|
443
|
+
settings: { ...settings, email: { ...settings.email, smtpPass: '' } },
|
|
444
|
+
auditLogs: auditLogs.logs
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
Logger.error('Failed to export data:', error);
|
|
449
|
+
res.status(500).json({ error: 'Failed to export data' });
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
/**
|
|
453
|
+
* POST /api/admin/backup
|
|
454
|
+
* Create a backup
|
|
455
|
+
*/
|
|
456
|
+
adminRouter.post('/backup', async (req, res) => {
|
|
457
|
+
try {
|
|
458
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
459
|
+
const userAgent = req.headers['user-agent'];
|
|
460
|
+
// Log backup action
|
|
461
|
+
await auditService.log({
|
|
462
|
+
userId: req.user?.id,
|
|
463
|
+
username: req.user?.username,
|
|
464
|
+
action: 'admin.view_logs',
|
|
465
|
+
ip,
|
|
466
|
+
userAgent,
|
|
467
|
+
details: { operation: 'backup_create' }
|
|
468
|
+
});
|
|
469
|
+
// Create backup filename
|
|
470
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
471
|
+
const filename = `archicore-backup-${timestamp}.json`;
|
|
472
|
+
Logger.info('Backup created:', filename);
|
|
473
|
+
res.json({ success: true, filename });
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
Logger.error('Failed to create backup:', error);
|
|
477
|
+
res.status(500).json({ error: 'Failed to create backup' });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
/**
|
|
481
|
+
* POST /api/admin/cache/clear
|
|
482
|
+
* Clear all cache
|
|
483
|
+
*/
|
|
484
|
+
adminRouter.post('/cache/clear', async (req, res) => {
|
|
485
|
+
try {
|
|
486
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
487
|
+
const userAgent = req.headers['user-agent'];
|
|
488
|
+
// TODO: Implement actual cache clearing (Redis, in-memory, etc.)
|
|
489
|
+
Logger.info('Cache clear requested by admin');
|
|
490
|
+
await auditService.log({
|
|
491
|
+
userId: req.user?.id,
|
|
492
|
+
username: req.user?.username,
|
|
493
|
+
action: 'admin.view_logs',
|
|
494
|
+
ip,
|
|
495
|
+
userAgent,
|
|
496
|
+
details: { operation: 'cache_clear' }
|
|
497
|
+
});
|
|
498
|
+
res.json({ success: true, message: 'Cache cleared' });
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
Logger.error('Failed to clear cache:', error);
|
|
502
|
+
res.status(500).json({ error: 'Failed to clear cache' });
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
/**
|
|
506
|
+
* POST /api/admin/analytics/reset
|
|
507
|
+
* Reset analytics data
|
|
508
|
+
*/
|
|
509
|
+
adminRouter.post('/analytics/reset', async (req, res) => {
|
|
510
|
+
try {
|
|
511
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
512
|
+
const userAgent = req.headers['user-agent'];
|
|
513
|
+
// TODO: Implement actual analytics reset
|
|
514
|
+
Logger.info('Analytics reset requested by admin');
|
|
515
|
+
await auditService.log({
|
|
516
|
+
userId: req.user?.id,
|
|
517
|
+
username: req.user?.username,
|
|
518
|
+
action: 'admin.view_logs',
|
|
519
|
+
ip,
|
|
520
|
+
userAgent,
|
|
521
|
+
details: { operation: 'analytics_reset' }
|
|
522
|
+
});
|
|
523
|
+
res.json({ success: true, message: 'Analytics reset' });
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
Logger.error('Failed to reset analytics:', error);
|
|
527
|
+
res.status(500).json({ error: 'Failed to reset analytics' });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
/**
|
|
531
|
+
* POST /api/admin/factory-reset
|
|
532
|
+
* Factory reset (dangerous!)
|
|
533
|
+
*/
|
|
534
|
+
adminRouter.post('/factory-reset', async (req, res) => {
|
|
535
|
+
try {
|
|
536
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
537
|
+
const userAgent = req.headers['user-agent'];
|
|
538
|
+
// Log before reset
|
|
539
|
+
await auditService.log({
|
|
540
|
+
userId: req.user?.id,
|
|
541
|
+
username: req.user?.username,
|
|
542
|
+
action: 'admin.user_delete',
|
|
543
|
+
ip,
|
|
544
|
+
userAgent,
|
|
545
|
+
details: { operation: 'factory_reset', severity: 'critical' }
|
|
546
|
+
});
|
|
547
|
+
// TODO: Implement actual factory reset
|
|
548
|
+
// This should:
|
|
549
|
+
// 1. Delete all users except the current admin
|
|
550
|
+
// 2. Clear all settings
|
|
551
|
+
// 3. Clear all cache
|
|
552
|
+
// 4. Clear audit logs
|
|
553
|
+
Logger.warn('Factory reset requested by admin:', req.user?.username);
|
|
554
|
+
res.json({ success: true, message: 'Factory reset initiated' });
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
Logger.error('Failed to factory reset:', error);
|
|
558
|
+
res.status(500).json({ error: 'Failed to factory reset' });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
123
561
|
//# sourceMappingURL=admin.js.map
|
|
@@ -302,6 +302,21 @@ apiRouter.post('/projects/:id/ask', authMiddleware, checkProjectAccess, async (r
|
|
|
302
302
|
res.status(400).json({ error: 'Question is required' });
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
|
+
// Input validation and sanitization
|
|
306
|
+
if (typeof question !== 'string') {
|
|
307
|
+
res.status(400).json({ error: 'Invalid question format' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// Limit question length to prevent abuse
|
|
311
|
+
const MAX_QUESTION_LENGTH = 5000;
|
|
312
|
+
if (question.length > MAX_QUESTION_LENGTH) {
|
|
313
|
+
res.status(400).json({ error: `Question too long. Maximum ${MAX_QUESTION_LENGTH} characters.` });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Sanitize question - remove null bytes and control characters (except newlines/tabs)
|
|
317
|
+
const sanitizedQuestion = question
|
|
318
|
+
.replace(/\0/g, '')
|
|
319
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
305
320
|
// Check request limit
|
|
306
321
|
if (userId) {
|
|
307
322
|
const usageResult = await authService.checkAndUpdateUsage(userId, 'request');
|
|
@@ -314,12 +329,12 @@ apiRouter.post('/projects/:id/ask', authMiddleware, checkProjectAccess, async (r
|
|
|
314
329
|
return;
|
|
315
330
|
}
|
|
316
331
|
}
|
|
317
|
-
const answer = await projectService.askArchitect(id,
|
|
332
|
+
const answer = await projectService.askArchitect(id, sanitizedQuestion, language || 'en');
|
|
318
333
|
res.json({ answer });
|
|
319
334
|
}
|
|
320
335
|
catch (error) {
|
|
321
336
|
Logger.error('Failed to ask architect:', error);
|
|
322
|
-
res.status(500).json({ error: 'Failed to
|
|
337
|
+
res.status(500).json({ error: 'Failed to process question' });
|
|
323
338
|
}
|
|
324
339
|
});
|
|
325
340
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import { AuthService } from '../services/auth-service.js';
|
|
6
|
+
import { auditService } from '../services/audit-service.js';
|
|
6
7
|
import { Logger } from '../../utils/logger.js';
|
|
7
8
|
export const authRouter = Router();
|
|
8
9
|
const authService = AuthService.getInstance();
|
|
@@ -35,6 +36,8 @@ export async function adminMiddleware(req, res, next) {
|
|
|
35
36
|
* Register new user
|
|
36
37
|
*/
|
|
37
38
|
authRouter.post('/register', async (req, res) => {
|
|
39
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
40
|
+
const userAgent = req.headers['user-agent'];
|
|
38
41
|
try {
|
|
39
42
|
const { email, username, password } = req.body;
|
|
40
43
|
if (!email || !username || !password) {
|
|
@@ -46,6 +49,17 @@ authRouter.post('/register', async (req, res) => {
|
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
const result = await authService.register(email, username, password);
|
|
52
|
+
// Audit log
|
|
53
|
+
if (result.success && result.user) {
|
|
54
|
+
await auditService.log({
|
|
55
|
+
userId: result.user.id,
|
|
56
|
+
username: result.user.username,
|
|
57
|
+
action: 'auth.register',
|
|
58
|
+
ip,
|
|
59
|
+
userAgent,
|
|
60
|
+
details: { email }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
49
63
|
res.json(result);
|
|
50
64
|
}
|
|
51
65
|
catch (error) {
|
|
@@ -58,6 +72,8 @@ authRouter.post('/register', async (req, res) => {
|
|
|
58
72
|
* Login with email/password
|
|
59
73
|
*/
|
|
60
74
|
authRouter.post('/login', async (req, res) => {
|
|
75
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
76
|
+
const userAgent = req.headers['user-agent'];
|
|
61
77
|
try {
|
|
62
78
|
const { email, password } = req.body;
|
|
63
79
|
if (!email || !password) {
|
|
@@ -65,10 +81,30 @@ authRouter.post('/login', async (req, res) => {
|
|
|
65
81
|
return;
|
|
66
82
|
}
|
|
67
83
|
const result = await authService.login(email, password);
|
|
84
|
+
// Audit log
|
|
85
|
+
await auditService.log({
|
|
86
|
+
userId: result.user?.id,
|
|
87
|
+
username: result.user?.username,
|
|
88
|
+
action: 'auth.login',
|
|
89
|
+
ip,
|
|
90
|
+
userAgent,
|
|
91
|
+
details: { email },
|
|
92
|
+
success: result.success,
|
|
93
|
+
errorMessage: result.success ? undefined : result.error
|
|
94
|
+
});
|
|
68
95
|
res.json(result);
|
|
69
96
|
}
|
|
70
97
|
catch (error) {
|
|
71
98
|
Logger.error('Login error:', error);
|
|
99
|
+
// Audit failed login attempt
|
|
100
|
+
await auditService.log({
|
|
101
|
+
action: 'auth.login',
|
|
102
|
+
ip,
|
|
103
|
+
userAgent,
|
|
104
|
+
details: { email: req.body.email },
|
|
105
|
+
success: false,
|
|
106
|
+
errorMessage: 'Login failed'
|
|
107
|
+
});
|
|
72
108
|
res.status(500).json({ success: false, error: 'Login failed' });
|
|
73
109
|
}
|
|
74
110
|
});
|
|
@@ -77,11 +113,21 @@ authRouter.post('/login', async (req, res) => {
|
|
|
77
113
|
* Logout current user
|
|
78
114
|
*/
|
|
79
115
|
authRouter.post('/logout', authMiddleware, async (req, res) => {
|
|
116
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
117
|
+
const userAgent = req.headers['user-agent'];
|
|
80
118
|
try {
|
|
81
119
|
const token = req.headers.authorization?.substring(7);
|
|
82
120
|
if (token) {
|
|
83
121
|
await authService.logout(token);
|
|
84
122
|
}
|
|
123
|
+
// Audit log
|
|
124
|
+
await auditService.log({
|
|
125
|
+
userId: req.user?.id,
|
|
126
|
+
username: req.user?.username,
|
|
127
|
+
action: 'auth.logout',
|
|
128
|
+
ip,
|
|
129
|
+
userAgent
|
|
130
|
+
});
|
|
85
131
|
res.json({ success: true });
|
|
86
132
|
}
|
|
87
133
|
catch (error) {
|
|
@@ -187,7 +187,7 @@ developerRouter.post('/billing/credits', authMiddleware, async (req, res) => {
|
|
|
187
187
|
res.status(400).json({ error: { message: 'Minimum amount is $5 (500 cents)' } });
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
|
-
// TODO: Интеграция с
|
|
190
|
+
// TODO: Интеграция с Revolut Business API
|
|
191
191
|
// Сейчас просто добавляем кредиты
|
|
192
192
|
const billing = await tokenService.addCredits(req.user.id, amount);
|
|
193
193
|
res.json({
|
|
@@ -52,7 +52,16 @@ router.post('/code', (_req, res) => {
|
|
|
52
52
|
pendingAuths.set(deviceCode, auth);
|
|
53
53
|
// Also index by user code for lookup
|
|
54
54
|
pendingAuths.set(userCode, auth);
|
|
55
|
-
|
|
55
|
+
// Use PUBLIC_URL if set, otherwise construct from environment
|
|
56
|
+
// Avoid using 0.0.0.0 as it's not a valid external address
|
|
57
|
+
let serverUrl = process.env.PUBLIC_URL;
|
|
58
|
+
if (!serverUrl) {
|
|
59
|
+
const host = process.env.HOST || 'localhost';
|
|
60
|
+
const port = process.env.PORT || 3000;
|
|
61
|
+
// If HOST is 0.0.0.0 (bind to all interfaces), use the server's actual IP or fallback to request origin
|
|
62
|
+
const effectiveHost = host === '0.0.0.0' ? (process.env.SERVER_IP || '194.156.66.251') : host;
|
|
63
|
+
serverUrl = `http://${effectiveHost}:${port}`;
|
|
64
|
+
}
|
|
56
65
|
res.json({
|
|
57
66
|
deviceCode,
|
|
58
67
|
userCode,
|
|
@@ -449,7 +449,7 @@ githubRouter.post('/repositories/:id/analyze', authMiddleware, async (req, res)
|
|
|
449
449
|
githubRouter.post('/webhook', async (req, res) => {
|
|
450
450
|
try {
|
|
451
451
|
const event = req.headers['x-github-event'];
|
|
452
|
-
|
|
452
|
+
const signature = req.headers['x-hub-signature-256'];
|
|
453
453
|
const payload = req.body;
|
|
454
454
|
if (!event || !payload) {
|
|
455
455
|
res.status(400).json({ error: 'Invalid webhook' });
|
|
@@ -467,9 +467,22 @@ githubRouter.post('/webhook', async (req, res) => {
|
|
|
467
467
|
res.status(200).json({ message: 'Repository not connected' });
|
|
468
468
|
return;
|
|
469
469
|
}
|
|
470
|
-
// Verify signature (
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
// Verify webhook signature (HMAC-SHA256)
|
|
471
|
+
if (repo.webhookSecret) {
|
|
472
|
+
if (!signature) {
|
|
473
|
+
Logger.warn(`Webhook missing signature for ${fullName}`);
|
|
474
|
+
res.status(401).json({ error: 'Missing signature' });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const secret = githubService.getWebhookSecret(repo.webhookSecret);
|
|
478
|
+
const payloadBody = JSON.stringify(payload);
|
|
479
|
+
if (!githubService.verifyWebhookSignature(payloadBody, signature, secret)) {
|
|
480
|
+
Logger.warn(`Invalid webhook signature for ${fullName}`);
|
|
481
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
Logger.debug(`Webhook signature verified for ${fullName}`);
|
|
485
|
+
}
|
|
473
486
|
Logger.info(`Webhook received: ${event} for ${fullName}`);
|
|
474
487
|
// Handle different events
|
|
475
488
|
switch (event) {
|