archicore 0.2.1 → 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.
@@ -3,6 +3,8 @@
3
3
  */
4
4
  import { Router } from 'express';
5
5
  import rateLimit from 'express-rate-limit';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
6
8
  import { AuthService } from '../services/auth-service.js';
7
9
  import { auditService } from '../services/audit-service.js';
8
10
  import { authMiddleware, adminMiddleware } from './auth.js';
@@ -25,7 +27,40 @@ const adminRateLimiter = rateLimit({
25
27
  });
26
28
  }
27
29
  });
28
- // All admin routes require authentication, admin role, and rate limiting
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
29
64
  adminRouter.use(authMiddleware);
30
65
  adminRouter.use(adminMiddleware);
31
66
  adminRouter.use(adminRateLimiter);
@@ -268,4 +303,259 @@ adminRouter.post('/audit/cleanup', async (req, res) => {
268
303
  res.status(500).json({ error: 'Failed to cleanup audit logs' });
269
304
  }
270
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
+ });
271
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
  /**
@@ -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,
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Report Issue Routes
3
+ *
4
+ * API for submitting bug reports and feedback
5
+ */
6
+ export declare const reportIssueRouter: import("express-serve-static-core").Router;
7
+ //# sourceMappingURL=report-issue.d.ts.map
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Report Issue Routes
3
+ *
4
+ * API for submitting bug reports and feedback
5
+ */
6
+ import { Router } from 'express';
7
+ import multer from 'multer';
8
+ import path from 'path';
9
+ import fs from 'fs/promises';
10
+ import { Logger } from '../../utils/logger.js';
11
+ export const reportIssueRouter = Router();
12
+ // Directory for storing reports
13
+ const REPORTS_DIR = process.env.REPORTS_DIR || path.join('.archicore', 'reports');
14
+ // File signature (magic bytes) validation
15
+ const FILE_SIGNATURES = {
16
+ 'image/png': { magic: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], ext: '.png' },
17
+ 'image/jpeg': { magic: [0xFF, 0xD8, 0xFF], ext: '.jpg' },
18
+ 'image/gif': { magic: [0x47, 0x49, 0x46, 0x38], ext: '.gif' }, // GIF87a or GIF89a
19
+ 'application/pdf': { magic: [0x25, 0x50, 0x44, 0x46], ext: '.pdf' } // %PDF
20
+ };
21
+ /**
22
+ * Validate file by checking magic bytes (file signature)
23
+ * Prevents MIME type spoofing attacks
24
+ */
25
+ function validateFileSignature(buffer) {
26
+ for (const [mimeType, { magic, ext }] of Object.entries(FILE_SIGNATURES)) {
27
+ if (buffer.length >= magic.length) {
28
+ const matches = magic.every((byte, index) => buffer[index] === byte);
29
+ if (matches) {
30
+ return { valid: true, detectedType: mimeType, ext };
31
+ }
32
+ }
33
+ }
34
+ return { valid: false, detectedType: null, ext: null };
35
+ }
36
+ /**
37
+ * Sanitize filename - remove path traversal attempts and dangerous characters
38
+ * @deprecated Use validated extension from FILE_SIGNATURES instead
39
+ */
40
+ function _sanitizeFilename(filename) {
41
+ // Remove path components
42
+ const basename = path.basename(filename);
43
+ // Remove null bytes and other dangerous characters
44
+ return basename
45
+ .replace(/\0/g, '')
46
+ .replace(/[<>:"/\\|?*]/g, '_')
47
+ .replace(/\.{2,}/g, '.'); // Prevent ..
48
+ }
49
+ void _sanitizeFilename; // Prevent unused warning
50
+ // Multer config for screenshot uploads
51
+ const upload = multer({
52
+ storage: multer.memoryStorage(),
53
+ limits: {
54
+ fileSize: 10 * 1024 * 1024 // 10MB max
55
+ },
56
+ fileFilter: (_req, file, cb) => {
57
+ // First check - MIME type (can be spoofed, but filters obvious garbage)
58
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'application/pdf'];
59
+ if (!allowedTypes.includes(file.mimetype)) {
60
+ cb(new Error('Invalid file type. Allowed: PNG, JPG, GIF, PDF'));
61
+ return;
62
+ }
63
+ // Real validation happens after upload when we can check magic bytes
64
+ cb(null, true);
65
+ }
66
+ });
67
+ /**
68
+ * POST /api/report-issue
69
+ * Submit a new issue report
70
+ */
71
+ reportIssueRouter.post('/', upload.single('screenshot'), async (req, res) => {
72
+ try {
73
+ const { description, category, priority, email, additionalInfo } = req.body;
74
+ // Validate required fields
75
+ if (!description || !category || !priority) {
76
+ res.status(400).json({
77
+ error: 'Missing required fields'
78
+ });
79
+ return;
80
+ }
81
+ // Whitelist validation - only allow known categories and priorities
82
+ const allowedCategories = ['bug', 'analysis', 'performance', 'ui', 'github', 'feature', 'other'];
83
+ const allowedPriorities = ['critical', 'high', 'medium', 'low'];
84
+ if (!allowedCategories.includes(category)) {
85
+ res.status(400).json({ error: 'Invalid category' });
86
+ return;
87
+ }
88
+ if (!allowedPriorities.includes(priority)) {
89
+ res.status(400).json({ error: 'Invalid priority' });
90
+ return;
91
+ }
92
+ // Basic email validation if provided
93
+ if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
94
+ res.status(400).json({ error: 'Invalid email format' });
95
+ return;
96
+ }
97
+ // Generate unique ID
98
+ const reportId = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
99
+ // Ensure reports directory exists
100
+ await fs.mkdir(REPORTS_DIR, { recursive: true });
101
+ // Save screenshot if provided (with security validation)
102
+ let screenshotFilename;
103
+ if (req.file) {
104
+ // Validate file signature (magic bytes) - prevents MIME spoofing
105
+ const validation = validateFileSignature(req.file.buffer);
106
+ if (!validation.valid) {
107
+ Logger.warn(`Invalid file signature detected for report ${reportId}. Claimed: ${req.file.mimetype}`);
108
+ res.status(400).json({
109
+ error: 'Invalid file content',
110
+ message: 'File does not match expected format. Please upload a valid PNG, JPG, GIF, or PDF.'
111
+ });
112
+ return;
113
+ }
114
+ // Use extension from validated file type, NOT from user input
115
+ const safeExt = validation.ext;
116
+ screenshotFilename = `${reportId}${safeExt}`;
117
+ const screenshotPath = path.join(REPORTS_DIR, 'screenshots', screenshotFilename);
118
+ // Verify path doesn't escape reports directory (defense in depth)
119
+ const resolvedPath = path.resolve(screenshotPath);
120
+ const screenshotsDir = path.resolve(REPORTS_DIR, 'screenshots');
121
+ if (!resolvedPath.startsWith(screenshotsDir)) {
122
+ Logger.warn(`Path traversal attempt detected for report ${reportId}`);
123
+ res.status(400).json({ error: 'Invalid request' });
124
+ return;
125
+ }
126
+ await fs.mkdir(path.join(REPORTS_DIR, 'screenshots'), { recursive: true });
127
+ await fs.writeFile(screenshotPath, req.file.buffer);
128
+ Logger.debug(`Screenshot saved for report ${reportId}`);
129
+ }
130
+ // Create report object - only store safe, non-sensitive data
131
+ const report = {
132
+ id: reportId,
133
+ description: description.substring(0, 10000), // Limit description length
134
+ category,
135
+ priority,
136
+ email: email || undefined,
137
+ additionalInfo: additionalInfo?.substring(0, 5000) || undefined,
138
+ hasScreenshot: !!screenshotFilename,
139
+ screenshotFilename, // Only filename, not full path
140
+ timestamp: new Date().toISOString(),
141
+ status: 'new'
142
+ };
143
+ // Save report to JSON file
144
+ const reportPath = path.join(REPORTS_DIR, `${reportId}.json`);
145
+ await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
146
+ Logger.info(`New issue report submitted: ${reportId} (${category}/${priority})`);
147
+ // Optional: Send notification (webhook, email, etc.)
148
+ await sendNotification(report);
149
+ res.json({
150
+ success: true,
151
+ reportId,
152
+ message: 'Report submitted successfully'
153
+ });
154
+ }
155
+ catch (error) {
156
+ // Log full error internally, but don't expose details to client
157
+ Logger.error('Failed to submit report:', error);
158
+ if (error instanceof multer.MulterError) {
159
+ if (error.code === 'LIMIT_FILE_SIZE') {
160
+ res.status(413).json({ error: 'Screenshot too large. Maximum size is 10MB.' });
161
+ return;
162
+ }
163
+ }
164
+ // Generic error message - never expose internal error details
165
+ res.status(500).json({
166
+ error: 'Failed to submit report. Please try again later.'
167
+ });
168
+ }
169
+ });
170
+ /**
171
+ * GET /api/report-issue/list
172
+ * Get list of reports (admin only)
173
+ */
174
+ reportIssueRouter.get('/list', async (req, res) => {
175
+ try {
176
+ // Basic auth check - in production use proper admin middleware
177
+ const adminKey = req.headers['x-admin-key'];
178
+ if (adminKey !== process.env.ADMIN_SECRET) {
179
+ res.status(403).json({ error: 'Admin access required' });
180
+ return;
181
+ }
182
+ const files = await fs.readdir(REPORTS_DIR).catch(() => []);
183
+ const reports = [];
184
+ for (const file of files) {
185
+ if (file.endsWith('.json')) {
186
+ const content = await fs.readFile(path.join(REPORTS_DIR, file), 'utf-8');
187
+ reports.push(JSON.parse(content));
188
+ }
189
+ }
190
+ // Sort by timestamp descending
191
+ reports.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
192
+ res.json({ reports, total: reports.length });
193
+ }
194
+ catch (error) {
195
+ Logger.error('Failed to list reports:', error);
196
+ res.status(500).json({ error: 'Failed to list reports' });
197
+ }
198
+ });
199
+ /**
200
+ * PATCH /api/report-issue/:id/status
201
+ * Update report status (admin only)
202
+ */
203
+ reportIssueRouter.patch('/:id/status', async (req, res) => {
204
+ try {
205
+ const adminKey = req.headers['x-admin-key'];
206
+ if (adminKey !== process.env.ADMIN_SECRET) {
207
+ res.status(403).json({ error: 'Admin access required' });
208
+ return;
209
+ }
210
+ const { id } = req.params;
211
+ const { status } = req.body;
212
+ if (!['new', 'reviewed', 'resolved'].includes(status)) {
213
+ res.status(400).json({ error: 'Invalid status' });
214
+ return;
215
+ }
216
+ const reportPath = path.join(REPORTS_DIR, `${id}.json`);
217
+ const content = await fs.readFile(reportPath, 'utf-8');
218
+ const report = JSON.parse(content);
219
+ report.status = status;
220
+ await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
221
+ res.json({ success: true, report });
222
+ }
223
+ catch (error) {
224
+ Logger.error('Failed to update report status:', error);
225
+ res.status(500).json({ error: 'Failed to update status' });
226
+ }
227
+ });
228
+ /**
229
+ * Send notification about new report (optional integrations)
230
+ */
231
+ async function sendNotification(report) {
232
+ // Discord webhook
233
+ const discordWebhook = process.env.DISCORD_WEBHOOK_URL;
234
+ if (discordWebhook) {
235
+ try {
236
+ const priorityColors = {
237
+ critical: 0xff0000,
238
+ high: 0xff6600,
239
+ medium: 0xffcc00,
240
+ low: 0x00cc00
241
+ };
242
+ await fetch(discordWebhook, {
243
+ method: 'POST',
244
+ headers: { 'Content-Type': 'application/json' },
245
+ body: JSON.stringify({
246
+ embeds: [{
247
+ title: `New Issue Report: ${report.category}`,
248
+ description: report.description.substring(0, 500),
249
+ color: priorityColors[report.priority] || 0x808080,
250
+ fields: [
251
+ { name: 'Priority', value: report.priority, inline: true },
252
+ { name: 'Category', value: report.category, inline: true },
253
+ { name: 'Report ID', value: report.id, inline: false }
254
+ ],
255
+ timestamp: report.timestamp
256
+ }]
257
+ })
258
+ });
259
+ }
260
+ catch (err) {
261
+ Logger.warn('Failed to send Discord notification:', err);
262
+ }
263
+ }
264
+ // Slack webhook
265
+ const slackWebhook = process.env.SLACK_WEBHOOK_URL;
266
+ if (slackWebhook) {
267
+ try {
268
+ await fetch(slackWebhook, {
269
+ method: 'POST',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({
272
+ text: `*New Issue Report*\n*Category:* ${report.category}\n*Priority:* ${report.priority}\n*Description:* ${report.description.substring(0, 200)}...`
273
+ })
274
+ });
275
+ }
276
+ catch (err) {
277
+ Logger.warn('Failed to send Slack notification:', err);
278
+ }
279
+ }
280
+ // Telegram bot
281
+ const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
282
+ const telegramChatId = process.env.TELEGRAM_CHAT_ID;
283
+ if (telegramToken && telegramChatId) {
284
+ try {
285
+ const priorityEmoji = {
286
+ critical: '🔴',
287
+ high: '🟠',
288
+ medium: '🟡',
289
+ low: '🟢'
290
+ };
291
+ const message = `${priorityEmoji[report.priority] || '⚪'} *New Issue Report*\n\n*Category:* ${report.category}\n*Priority:* ${report.priority}\n*Description:* ${report.description.substring(0, 300)}...\n\n_ID: ${report.id}_`;
292
+ await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({
296
+ chat_id: telegramChatId,
297
+ text: message,
298
+ parse_mode: 'Markdown'
299
+ })
300
+ });
301
+ }
302
+ catch (err) {
303
+ Logger.warn('Failed to send Telegram notification:', err);
304
+ }
305
+ }
306
+ }
307
+ //# sourceMappingURL=report-issue.js.map