archicore 0.3.5 → 0.3.7

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.
@@ -11,6 +11,7 @@ import { ExportManager } from '../../export/index.js';
11
11
  import { authMiddleware } from './auth.js';
12
12
  import { FileUtils } from '../../utils/file-utils.js';
13
13
  import { taskQueue } from '../services/task-queue.js';
14
+ import { EnterpriseIndexer, TIER_CONFIG } from '../services/enterprise-indexer.js';
14
15
  export const apiRouter = Router();
15
16
  // Singleton сервиса проектов
16
17
  const projectService = new ProjectService();
@@ -761,8 +762,10 @@ apiRouter.post('/projects/:id/documentation', authMiddleware, checkProjectAccess
761
762
  */
762
763
  apiRouter.get('/projects/:id/index-progress', async (req, res) => {
763
764
  const { id } = req.params;
764
- // Handle auth via query param (EventSource doesn't support headers)
765
- const token = req.query.token || req.headers.authorization?.substring(7);
765
+ // Handle auth via query param, header, or cookie (EventSource doesn't support custom headers)
766
+ const token = req.query.token
767
+ || req.headers.authorization?.substring(7)
768
+ || req.cookies?.archicore_token;
766
769
  if (!token) {
767
770
  res.status(401).json({ error: 'Authentication required' });
768
771
  return;
@@ -1035,6 +1038,243 @@ apiRouter.get('/queue/stats', authMiddleware, async (req, res) => {
1035
1038
  res.status(500).json({ error: 'Failed to get queue stats' });
1036
1039
  }
1037
1040
  });
1041
+ // ==================== ENTERPRISE ENDPOINTS ====================
1042
+ /**
1043
+ * GET /api/projects/:id/enterprise/estimate
1044
+ * Get estimation for enterprise-scale project analysis
1045
+ */
1046
+ apiRouter.get('/projects/:id/enterprise/estimate', authMiddleware, checkProjectAccess, async (req, res) => {
1047
+ try {
1048
+ const { id } = req.params;
1049
+ const project = await projectService.getProject(id);
1050
+ if (!project) {
1051
+ res.status(404).json({ error: 'Project not found' });
1052
+ return;
1053
+ }
1054
+ const indexer = new EnterpriseIndexer(project.path);
1055
+ await indexer.initialize();
1056
+ const estimate = await indexer.getProjectSize();
1057
+ res.json({
1058
+ projectId: id,
1059
+ projectName: project.name,
1060
+ ...estimate,
1061
+ tiers: {
1062
+ quick: {
1063
+ ...TIER_CONFIG.quick,
1064
+ estimatedFiles: Math.min(estimate.totalFiles, TIER_CONFIG.quick.maxFiles),
1065
+ },
1066
+ standard: {
1067
+ ...TIER_CONFIG.standard,
1068
+ estimatedFiles: Math.min(estimate.totalFiles, TIER_CONFIG.standard.maxFiles),
1069
+ },
1070
+ deep: {
1071
+ ...TIER_CONFIG.deep,
1072
+ estimatedFiles: Math.min(estimate.totalFiles, TIER_CONFIG.deep.maxFiles),
1073
+ },
1074
+ },
1075
+ });
1076
+ }
1077
+ catch (error) {
1078
+ Logger.error('Failed to estimate project:', error);
1079
+ res.status(500).json({ error: 'Failed to estimate project' });
1080
+ }
1081
+ });
1082
+ /**
1083
+ * POST /api/projects/:id/enterprise/index
1084
+ * Index large project with enterprise options
1085
+ */
1086
+ apiRouter.post('/projects/:id/enterprise/index', authMiddleware, checkProjectAccess, async (req, res) => {
1087
+ try {
1088
+ const { id } = req.params;
1089
+ const userId = getUserId(req);
1090
+ const options = req.body;
1091
+ // Check tier access
1092
+ if (options.tier === 'deep' && req.user?.tier !== 'enterprise') {
1093
+ res.status(403).json({
1094
+ error: 'Deep analysis requires Enterprise tier',
1095
+ upgradeUrl: '/pricing'
1096
+ });
1097
+ return;
1098
+ }
1099
+ const project = await projectService.getProject(id);
1100
+ if (!project) {
1101
+ res.status(404).json({ error: 'Project not found' });
1102
+ return;
1103
+ }
1104
+ // Create async task for enterprise indexing
1105
+ const task = taskQueue.createTask('enterprise-index', id, userId || 'anonymous');
1106
+ // Store options in task
1107
+ task.enterpriseOptions = options;
1108
+ res.json({
1109
+ taskId: task.id,
1110
+ status: task.status,
1111
+ message: 'Enterprise indexing task queued. Poll /api/tasks/:taskId for status.',
1112
+ options: {
1113
+ tier: options.tier || 'standard',
1114
+ sampling: options.sampling,
1115
+ incremental: options.incremental,
1116
+ },
1117
+ });
1118
+ }
1119
+ catch (error) {
1120
+ Logger.error('Failed to start enterprise indexing:', error);
1121
+ res.status(500).json({ error: 'Failed to start enterprise indexing' });
1122
+ }
1123
+ });
1124
+ /**
1125
+ * GET /api/projects/:id/enterprise/files
1126
+ * Get list of files that would be analyzed based on current options
1127
+ */
1128
+ apiRouter.get('/projects/:id/enterprise/files', authMiddleware, checkProjectAccess, async (req, res) => {
1129
+ try {
1130
+ const { id } = req.params;
1131
+ const tier = req.query.tier || 'standard';
1132
+ const strategy = req.query.strategy || 'smart';
1133
+ const project = await projectService.getProject(id);
1134
+ if (!project) {
1135
+ res.status(404).json({ error: 'Project not found' });
1136
+ return;
1137
+ }
1138
+ const indexer = new EnterpriseIndexer(project.path, {
1139
+ tier,
1140
+ sampling: {
1141
+ enabled: true,
1142
+ maxFiles: TIER_CONFIG[tier].maxFiles,
1143
+ strategy
1144
+ }
1145
+ });
1146
+ await indexer.initialize();
1147
+ const files = await indexer.getFilesToIndex();
1148
+ // Return relative paths
1149
+ const relativePaths = files.map(f => {
1150
+ const rel = f.replace(project.path, '').replace(/^[\/\\]/, '');
1151
+ return rel;
1152
+ });
1153
+ res.json({
1154
+ projectId: id,
1155
+ tier,
1156
+ strategy,
1157
+ totalSelected: files.length,
1158
+ maxAllowed: TIER_CONFIG[tier].maxFiles,
1159
+ files: relativePaths.slice(0, 100), // Return first 100 for preview
1160
+ hasMore: relativePaths.length > 100
1161
+ });
1162
+ }
1163
+ catch (error) {
1164
+ Logger.error('Failed to get enterprise files:', error);
1165
+ res.status(500).json({ error: 'Failed to get files' });
1166
+ }
1167
+ });
1168
+ /**
1169
+ * POST /api/projects/:id/enterprise/incremental
1170
+ * Run incremental indexing (only changed files)
1171
+ */
1172
+ apiRouter.post('/projects/:id/enterprise/incremental', authMiddleware, checkProjectAccess, async (req, res) => {
1173
+ try {
1174
+ const { id } = req.params;
1175
+ const { since } = req.body;
1176
+ if (!since) {
1177
+ res.status(400).json({ error: 'since parameter required (ISO date or commit hash)' });
1178
+ return;
1179
+ }
1180
+ const project = await projectService.getProject(id);
1181
+ if (!project) {
1182
+ res.status(404).json({ error: 'Project not found' });
1183
+ return;
1184
+ }
1185
+ const indexer = new EnterpriseIndexer(project.path, {
1186
+ tier: 'standard',
1187
+ incremental: { enabled: true, since },
1188
+ sampling: { enabled: false, maxFiles: 50000, strategy: 'smart' }
1189
+ });
1190
+ await indexer.initialize();
1191
+ const changedFiles = await indexer.getChangedFiles(since);
1192
+ if (changedFiles.length === 0) {
1193
+ res.json({
1194
+ message: 'No changed files found',
1195
+ since,
1196
+ changedFiles: 0
1197
+ });
1198
+ return;
1199
+ }
1200
+ // Return changed files info
1201
+ const relativePaths = changedFiles.map(f => f.replace(project.path, '').replace(/^[\/\\]/, ''));
1202
+ res.json({
1203
+ projectId: id,
1204
+ since,
1205
+ changedFiles: changedFiles.length,
1206
+ files: relativePaths.slice(0, 50),
1207
+ hasMore: relativePaths.length > 50,
1208
+ message: `Found ${changedFiles.length} changed files. Use POST /api/projects/:id/enterprise/index with incremental.enabled=true to index them.`
1209
+ });
1210
+ }
1211
+ catch (error) {
1212
+ Logger.error('Failed to check incremental changes:', error);
1213
+ res.status(500).json({ error: 'Failed to check incremental changes' });
1214
+ }
1215
+ });
1216
+ /**
1217
+ * GET /api/enterprise/tiers
1218
+ * Get available analysis tiers info
1219
+ */
1220
+ apiRouter.get('/enterprise/tiers', async (_req, res) => {
1221
+ res.json({
1222
+ tiers: TIER_CONFIG,
1223
+ recommendations: {
1224
+ quick: 'Best for initial exploration of large projects (50K+ files)',
1225
+ standard: 'Recommended for most projects - balances speed and depth',
1226
+ deep: 'For critical projects requiring comprehensive analysis (Enterprise only)'
1227
+ },
1228
+ samplingStrategies: {
1229
+ smart: 'AI-powered selection based on importance, imports, and git activity',
1230
+ 'hot-files': 'Files with most git commits in the last year',
1231
+ 'directory-balanced': 'Equal representation from each directory',
1232
+ random: 'Random sampling'
1233
+ }
1234
+ });
1235
+ });
1236
+ // Register enterprise indexing task executor
1237
+ taskQueue.registerExecutor('enterprise-index', async (task, updateProgress) => {
1238
+ try {
1239
+ const project = await projectService.getProject(task.projectId);
1240
+ if (!project) {
1241
+ return { success: false, error: 'Project not found' };
1242
+ }
1243
+ const options = task.enterpriseOptions || {};
1244
+ updateProgress({ phase: 'initializing', current: 5, message: 'Initializing enterprise indexer...' });
1245
+ const indexer = new EnterpriseIndexer(project.path, options);
1246
+ await indexer.initialize();
1247
+ updateProgress({ phase: 'scanning', current: 10, message: 'Scanning project files...' });
1248
+ const files = await indexer.getFilesToIndex();
1249
+ const tierConfig = indexer.getTierConfig();
1250
+ updateProgress({
1251
+ phase: 'indexing',
1252
+ current: 15,
1253
+ message: `Selected ${files.length} files using ${options.sampling?.strategy || 'smart'} strategy`
1254
+ });
1255
+ // Return the selected files info - actual indexing happens in project service
1256
+ // This task prepares the files, project service handles the rest
1257
+ return {
1258
+ success: true,
1259
+ data: {
1260
+ tier: options.tier || 'standard',
1261
+ selectedFiles: files.length,
1262
+ maxFiles: tierConfig.maxFiles,
1263
+ strategy: options.sampling?.strategy || 'smart',
1264
+ skipEmbeddings: tierConfig.skipEmbeddings,
1265
+ skipSecurity: tierConfig.skipSecurity,
1266
+ skipDuplication: tierConfig.skipDuplication,
1267
+ files: files.slice(0, 100), // First 100 for reference
1268
+ }
1269
+ };
1270
+ }
1271
+ catch (error) {
1272
+ return {
1273
+ success: false,
1274
+ error: error instanceof Error ? error.message : String(error),
1275
+ };
1276
+ }
1277
+ });
1038
1278
  // Helper function to format time
1039
1279
  function formatTime(seconds) {
1040
1280
  if (seconds < 60) {
@@ -4,6 +4,7 @@
4
4
  import { Request, Response, NextFunction } from 'express';
5
5
  import { User as ArchiCoreUser } from '../../types/user.js';
6
6
  export declare const authRouter: import("express-serve-static-core").Router;
7
+ export declare function cleanupAuthIntervals(): void;
7
8
  declare global {
8
9
  namespace Express {
9
10
  interface Request {
@@ -2,6 +2,7 @@
2
2
  * Authentication API Routes for ArchiCore
3
3
  */
4
4
  import { Router } from 'express';
5
+ import crypto from 'crypto';
5
6
  import { AuthService } from '../services/auth-service.js';
6
7
  import { auditService } from '../services/audit-service.js';
7
8
  import { Logger } from '../../utils/logger.js';
@@ -64,12 +65,14 @@ async function deleteVerificationCode(email) {
64
65
  await cache.del(key);
65
66
  verificationCodes.delete(email);
66
67
  }
67
- // Generate 6-digit verification code
68
+ // Generate 6-digit verification code using cryptographically secure random
68
69
  function generateVerificationCode() {
69
- return Math.floor(100000 + Math.random() * 900000).toString();
70
+ // Use crypto.randomInt for secure random number generation
71
+ const code = crypto.randomInt(100000, 1000000);
72
+ return code.toString();
70
73
  }
71
74
  // Cleanup expired memory codes
72
- setInterval(() => {
75
+ const cleanupIntervalId = setInterval(() => {
73
76
  const now = Date.now();
74
77
  for (const [email, data] of verificationCodes.entries()) {
75
78
  if (data.expiresAt < now) {
@@ -77,6 +80,11 @@ setInterval(() => {
77
80
  }
78
81
  }
79
82
  }, 60 * 1000); // Clean up every minute
83
+ // Export cleanup function for graceful shutdown
84
+ export function cleanupAuthIntervals() {
85
+ clearInterval(cleanupIntervalId);
86
+ Logger.info('Auth cleanup intervals cleared');
87
+ }
80
88
  // Middleware to check authentication (supports both Bearer token and httpOnly cookie)
81
89
  export async function authMiddleware(req, res, next) {
82
90
  const authHeader = req.headers.authorization;
@@ -10,7 +10,9 @@
10
10
  import { Router } from 'express';
11
11
  import { mkdir } from 'fs/promises';
12
12
  import { join } from 'path';
13
- import AdmZip from 'adm-zip';
13
+ import { Readable } from 'stream';
14
+ import { pipeline } from 'stream/promises';
15
+ import * as tar from 'tar';
14
16
  import { GitLabService } from '../../gitlab/gitlab-service.js';
15
17
  import { ProjectService } from '../services/project-service.js';
16
18
  import { AuthService } from '../services/auth-service.js';
@@ -106,31 +108,18 @@ gitlabRouter.post('/connect', authMiddleware, async (req, res) => {
106
108
  // Download and create ArchiCore project
107
109
  const targetBranch = branch || project.default_branch || 'main';
108
110
  Logger.progress(`Downloading GitLab repository: ${project.path_with_namespace} (branch: ${targetBranch})`);
109
- const zipBuffer = await gitlabService.downloadRepository(req.user.id, instance.id, project.id, targetBranch);
111
+ const tarGzBuffer = await gitlabService.downloadRepository(req.user.id, instance.id, project.id, targetBranch);
112
+ Logger.info(`Downloaded tar.gz: ${tarGzBuffer.length} bytes`);
110
113
  // Create projects directory
111
114
  const projectsDir = process.env.PROJECTS_DIR || join('.archicore', 'projects');
112
115
  await mkdir(projectsDir, { recursive: true });
113
- // Extract to project directory
116
+ // Extract tar.gz to project directory
114
117
  const projectPath = join(projectsDir, project.name);
115
118
  await mkdir(projectPath, { recursive: true });
116
- const zip = new AdmZip(zipBuffer);
117
- const zipEntries = zip.getEntries();
118
- // Extract files
119
- for (const entry of zipEntries) {
120
- if (entry.isDirectory) {
121
- const dirPath = join(projectPath, entry.entryName);
122
- await mkdir(dirPath, { recursive: true });
123
- }
124
- else {
125
- const filePath = join(projectPath, entry.entryName);
126
- const fileDir = join(projectPath, entry.entryName.split('/').slice(0, -1).join('/'));
127
- await mkdir(fileDir, { recursive: true });
128
- const content = entry.getData();
129
- const { writeFile } = await import('fs/promises');
130
- await writeFile(filePath, content);
131
- }
132
- }
133
- // GitLab creates a folder like "project-branch" inside, find it
119
+ // Extract tar.gz archive from buffer
120
+ const tarStream = Readable.from(tarGzBuffer);
121
+ await pipeline(tarStream, tar.extract({ cwd: projectPath }));
122
+ // GitLab creates a folder like "project-branch-sha" inside, find it
134
123
  const { readdir, stat } = await import('fs/promises');
135
124
  const contents = await readdir(projectPath);
136
125
  let actualPath = projectPath;
@@ -390,36 +379,19 @@ gitlabRouter.post('/repositories/connect', authMiddleware, async (req, res) => {
390
379
  const project = await gitlabService.getProject(req.user.id, instanceId, projectId);
391
380
  const targetBranch = branch || project.default_branch || 'main';
392
381
  Logger.progress(`Downloading GitLab repository: ${project.path_with_namespace} (branch: ${targetBranch})`);
393
- const zipBuffer = await gitlabService.downloadRepository(req.user.id, instanceId, projectId, targetBranch);
394
- Logger.info(`Downloaded ZIP: ${zipBuffer.length} bytes`);
382
+ const tarGzBuffer = await gitlabService.downloadRepository(req.user.id, instanceId, projectId, targetBranch);
383
+ Logger.info(`Downloaded tar.gz: ${tarGzBuffer.length} bytes`);
395
384
  // Create projects directory
396
385
  const projectsDir = process.env.PROJECTS_DIR || join('.archicore', 'projects');
397
386
  await mkdir(projectsDir, { recursive: true });
398
- // Extract to project directory
387
+ // Extract tar.gz to project directory
399
388
  const projectPath = join(projectsDir, project.name);
400
389
  await mkdir(projectPath, { recursive: true });
401
- const zip = new AdmZip(zipBuffer);
402
- const zipEntries = zip.getEntries();
403
- Logger.info(`ZIP contains ${zipEntries.length} entries`);
404
- // Extract files
405
- let extractedCount = 0;
406
- for (const entry of zipEntries) {
407
- if (entry.isDirectory) {
408
- const dirPath = join(projectPath, entry.entryName);
409
- await mkdir(dirPath, { recursive: true });
410
- }
411
- else {
412
- const filePath = join(projectPath, entry.entryName);
413
- const fileDir = join(projectPath, entry.entryName.split('/').slice(0, -1).join('/'));
414
- await mkdir(fileDir, { recursive: true });
415
- const content = entry.getData();
416
- const { writeFile } = await import('fs/promises');
417
- await writeFile(filePath, content);
418
- extractedCount++;
419
- }
420
- }
421
- Logger.info(`Extraction complete: ${extractedCount} files extracted`);
422
- // GitLab creates a folder like "project-branch" inside, find it
390
+ // Extract tar.gz archive from buffer
391
+ const tarStream = Readable.from(tarGzBuffer);
392
+ await pipeline(tarStream, tar.extract({ cwd: projectPath }));
393
+ Logger.info(`Extraction complete`);
394
+ // GitLab creates a folder like "project-branch-sha" inside, find it
423
395
  const { readdir, stat } = await import('fs/promises');
424
396
  const contents = await readdir(projectPath);
425
397
  let actualPath = projectPath;
@@ -7,7 +7,31 @@ import { Router } from 'express';
7
7
  import multer from 'multer';
8
8
  import path from 'path';
9
9
  import fs from 'fs/promises';
10
+ import nodemailer from 'nodemailer';
10
11
  import { Logger } from '../../utils/logger.js';
12
+ // SMTP transporter (lazy initialized)
13
+ let mailTransporter = null;
14
+ function getMailTransporter() {
15
+ if (mailTransporter)
16
+ return mailTransporter;
17
+ const smtpHost = process.env.SMTP_HOST;
18
+ const smtpPort = process.env.SMTP_PORT;
19
+ const smtpUser = process.env.SMTP_USER;
20
+ const smtpPass = process.env.SMTP_PASS;
21
+ if (!smtpHost || !smtpUser || !smtpPass) {
22
+ return null;
23
+ }
24
+ mailTransporter = nodemailer.createTransport({
25
+ host: smtpHost,
26
+ port: parseInt(smtpPort || '465', 10),
27
+ secure: process.env.SMTP_SECURE !== 'false',
28
+ auth: {
29
+ user: smtpUser,
30
+ pass: smtpPass,
31
+ },
32
+ });
33
+ return mailTransporter;
34
+ }
11
35
  export const reportIssueRouter = Router();
12
36
  // Directory for storing reports
13
37
  const REPORTS_DIR = process.env.REPORTS_DIR || path.join('.archicore', 'reports');
@@ -229,6 +253,85 @@ reportIssueRouter.patch('/:id/status', async (req, res) => {
229
253
  * Send notification about new report (optional integrations)
230
254
  */
231
255
  async function sendNotification(report) {
256
+ // Email notification (primary)
257
+ const transporter = getMailTransporter();
258
+ const emailTo = process.env.REPORT_EMAIL || process.env.SMTP_USER || 'info@archicore.io';
259
+ const emailFrom = process.env.EMAIL_FROM || process.env.SMTP_USER;
260
+ const emailFromName = process.env.EMAIL_FROM_NAME || 'ArchiCore';
261
+ if (transporter && emailFrom) {
262
+ try {
263
+ const priorityColors = {
264
+ critical: '#ff0000',
265
+ high: '#ff6600',
266
+ medium: '#ffcc00',
267
+ low: '#00cc00'
268
+ };
269
+ const htmlContent = `
270
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
271
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 8px 8px 0 0;">
272
+ <h1 style="color: white; margin: 0;">New Issue Report</h1>
273
+ </div>
274
+ <div style="background: #f9f9f9; padding: 20px; border: 1px solid #ddd; border-top: none;">
275
+ <table style="width: 100%; border-collapse: collapse;">
276
+ <tr>
277
+ <td style="padding: 10px; border-bottom: 1px solid #eee;"><strong>Report ID:</strong></td>
278
+ <td style="padding: 10px; border-bottom: 1px solid #eee; font-family: monospace;">${report.id}</td>
279
+ </tr>
280
+ <tr>
281
+ <td style="padding: 10px; border-bottom: 1px solid #eee;"><strong>Category:</strong></td>
282
+ <td style="padding: 10px; border-bottom: 1px solid #eee;">${report.category}</td>
283
+ </tr>
284
+ <tr>
285
+ <td style="padding: 10px; border-bottom: 1px solid #eee;"><strong>Priority:</strong></td>
286
+ <td style="padding: 10px; border-bottom: 1px solid #eee;">
287
+ <span style="background: ${priorityColors[report.priority] || '#808080'}; color: white; padding: 3px 8px; border-radius: 4px; font-weight: bold;">
288
+ ${report.priority.toUpperCase()}
289
+ </span>
290
+ </td>
291
+ </tr>
292
+ ${report.email ? `
293
+ <tr>
294
+ <td style="padding: 10px; border-bottom: 1px solid #eee;"><strong>Reporter Email:</strong></td>
295
+ <td style="padding: 10px; border-bottom: 1px solid #eee;"><a href="mailto:${report.email}">${report.email}</a></td>
296
+ </tr>
297
+ ` : ''}
298
+ <tr>
299
+ <td style="padding: 10px; border-bottom: 1px solid #eee;"><strong>Submitted:</strong></td>
300
+ <td style="padding: 10px; border-bottom: 1px solid #eee;">${new Date(report.timestamp).toLocaleString()}</td>
301
+ </tr>
302
+ </table>
303
+ <div style="margin-top: 20px; padding: 15px; background: white; border: 1px solid #ddd; border-radius: 4px;">
304
+ <strong>Description:</strong>
305
+ <p style="white-space: pre-wrap; margin: 10px 0 0 0;">${report.description}</p>
306
+ </div>
307
+ ${report.additionalInfo ? `
308
+ <div style="margin-top: 15px; padding: 15px; background: white; border: 1px solid #ddd; border-radius: 4px;">
309
+ <strong>Additional Info:</strong>
310
+ <p style="white-space: pre-wrap; margin: 10px 0 0 0;">${report.additionalInfo}</p>
311
+ </div>
312
+ ` : ''}
313
+ ${report.hasScreenshot ? `
314
+ <p style="margin-top: 15px; color: #666;"><em>📎 Screenshot attached to this report</em></p>
315
+ ` : ''}
316
+ </div>
317
+ <div style="background: #333; color: #999; padding: 15px; border-radius: 0 0 8px 8px; text-align: center; font-size: 12px;">
318
+ ArchiCore Issue Tracker - Automated Notification
319
+ </div>
320
+ </div>
321
+ `;
322
+ await transporter.sendMail({
323
+ from: `"${emailFromName}" <${emailFrom}>`,
324
+ to: emailTo,
325
+ subject: `[${report.priority.toUpperCase()}] New Issue Report: ${report.category}`,
326
+ html: htmlContent,
327
+ text: `New Issue Report\n\nID: ${report.id}\nCategory: ${report.category}\nPriority: ${report.priority}\nEmail: ${report.email || 'Not provided'}\n\nDescription:\n${report.description}\n\nAdditional Info:\n${report.additionalInfo || 'None'}`
328
+ });
329
+ Logger.info(`Email notification sent for report ${report.id}`);
330
+ }
331
+ catch (err) {
332
+ Logger.warn('Failed to send email notification:', err);
333
+ }
334
+ }
232
335
  // Discord webhook
233
336
  const discordWebhook = process.env.DISCORD_WEBHOOK_URL;
234
337
  if (discordWebhook) {
@@ -111,7 +111,7 @@ uploadRouter.post('/', authMiddleware, upload.single('file'), async (req, res) =
111
111
  * - threats: SecurityThreat[]
112
112
  * - warnings: string[]
113
113
  */
114
- uploadRouter.post('/scan', upload.single('file'), async (req, res) => {
114
+ uploadRouter.post('/scan', authMiddleware, upload.single('file'), async (req, res) => {
115
115
  try {
116
116
  if (!req.file) {
117
117
  res.status(400).json({ error: 'No file uploaded' });
@@ -151,7 +151,7 @@ uploadRouter.post('/scan', upload.single('file'), async (req, res) => {
151
151
  * GET /api/upload/projects
152
152
  * Получить список загруженных проектов
153
153
  */
154
- uploadRouter.get('/projects', async (_req, res) => {
154
+ uploadRouter.get('/projects', authMiddleware, async (_req, res) => {
155
155
  try {
156
156
  const projects = await uploadService.listUploadedProjects();
157
157
  res.json({ projects });
@@ -165,7 +165,7 @@ uploadRouter.get('/projects', async (_req, res) => {
165
165
  * DELETE /api/upload/:uploadId
166
166
  * Удалить загруженный проект
167
167
  */
168
- uploadRouter.delete('/:uploadId', async (req, res) => {
168
+ uploadRouter.delete('/:uploadId', authMiddleware, async (req, res) => {
169
169
  try {
170
170
  const { uploadId } = req.params;
171
171
  await uploadService.deleteProject(uploadId);
@@ -19,14 +19,27 @@ class EncryptionService {
19
19
  if (this.initialized)
20
20
  return;
21
21
  const encryptionKey = process.env.ENCRYPTION_KEY;
22
+ const encryptionSalt = process.env.ENCRYPTION_SALT;
23
+ // SECURITY: Require encryption key in production
22
24
  if (!encryptionKey) {
25
+ if (process.env.NODE_ENV === 'production') {
26
+ Logger.error('CRITICAL: ENCRYPTION_KEY is required in production');
27
+ throw new Error('ENCRYPTION_KEY environment variable is required');
28
+ }
23
29
  Logger.warn('ENCRYPTION_KEY not set - generating temporary key (NOT FOR PRODUCTION)');
24
- // Generate a temporary key for development
25
30
  this.key = crypto.randomBytes(32);
26
31
  }
27
32
  else {
33
+ // SECURITY: Require salt in production
34
+ if (!encryptionSalt) {
35
+ if (process.env.NODE_ENV === 'production') {
36
+ Logger.error('CRITICAL: ENCRYPTION_SALT is required in production');
37
+ throw new Error('ENCRYPTION_SALT environment variable is required');
38
+ }
39
+ Logger.warn('ENCRYPTION_SALT not set - using random salt (NOT FOR PRODUCTION)');
40
+ }
28
41
  // Derive key from password using PBKDF2
29
- const salt = process.env.ENCRYPTION_SALT || 'archicore-default-salt';
42
+ const salt = encryptionSalt || crypto.randomBytes(32).toString('hex');
30
43
  this.key = crypto.pbkdf2Sync(encryptionKey, salt, 100000, 32, 'sha256');
31
44
  }
32
45
  this.initialized = true;
@@ -93,9 +106,12 @@ class EncryptionService {
93
106
  * Hash a value (one-way, for comparison)
94
107
  */
95
108
  hash(value) {
96
- const salt = process.env.ENCRYPTION_SALT || 'archicore-default-salt';
109
+ const salt = process.env.ENCRYPTION_SALT;
110
+ if (!salt && process.env.NODE_ENV === 'production') {
111
+ throw new Error('ENCRYPTION_SALT is required for hashing in production');
112
+ }
97
113
  return crypto
98
- .createHmac('sha256', salt)
114
+ .createHmac('sha256', salt || 'dev-only-salt')
99
115
  .update(value)
100
116
  .digest('hex');
101
117
  }