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.
@@ -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
- // All admin routes require authentication and admin role
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, question, language || 'en');
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 ask architect' });
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: Интеграция с Stripe/PayPal
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
- const serverUrl = process.env.PUBLIC_URL || `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`;
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
- // signature available at req.headers['x-hub-signature-256'] for verification
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 (if webhook secret is set)
471
- // Note: In production, you'd verify the signature properly using:
472
- // githubService.verifyWebhookSignature(JSON.stringify(req.body), signature, secret)
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) {