archicore 0.3.6 → 0.3.8
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/interactive.js +220 -1
- package/dist/github/github-service.d.ts +4 -4
- package/dist/github/github-service.js +29 -13
- package/dist/gitlab/gitlab-service.js +48 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -9
- package/dist/orchestrator/index.js +72 -7
- package/dist/server/index.js +68 -1
- package/dist/server/routes/api.js +242 -2
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +11 -3
- package/dist/server/routes/gitlab.js +18 -46
- package/dist/server/routes/report-issue.js +103 -0
- package/dist/server/routes/upload.js +3 -3
- package/dist/server/services/encryption.js +20 -4
- package/dist/server/services/enterprise-indexer.d.ts +116 -0
- package/dist/server/services/enterprise-indexer.js +529 -0
- package/dist/server/services/project-service.js +14 -5
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/project-analyzer.d.ts +13 -0
- package/dist/utils/project-analyzer.js +130 -0
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'url';
|
|
|
21
21
|
import { Logger } from '../utils/logger.js';
|
|
22
22
|
import { apiRouter } from './routes/api.js';
|
|
23
23
|
import { uploadRouter } from './routes/upload.js';
|
|
24
|
-
import { authRouter } from './routes/auth.js';
|
|
24
|
+
import { authRouter, cleanupAuthIntervals } from './routes/auth.js';
|
|
25
25
|
import { oauthRouter } from './routes/oauth.js';
|
|
26
26
|
import { adminRouter } from './routes/admin.js';
|
|
27
27
|
import { developerRouter } from './routes/developer.js';
|
|
@@ -159,6 +159,15 @@ export class ArchiCoreServer {
|
|
|
159
159
|
this.setupErrorHandling();
|
|
160
160
|
}
|
|
161
161
|
setupMiddleware() {
|
|
162
|
+
// Trust proxy - required when running behind nginx/reverse proxy
|
|
163
|
+
// This allows express-rate-limit to correctly identify clients via X-Forwarded-For
|
|
164
|
+
// Set TRUST_PROXY=false to disable, or specify number of proxies (e.g., "1", "2")
|
|
165
|
+
const trustProxy = process.env.TRUST_PROXY;
|
|
166
|
+
if (trustProxy !== 'false') {
|
|
167
|
+
// Default: trust first proxy (typical setup with nginx)
|
|
168
|
+
const proxyCount = trustProxy ? parseInt(trustProxy, 10) : 1;
|
|
169
|
+
this.app.set('trust proxy', isNaN(proxyCount) ? trustProxy : proxyCount);
|
|
170
|
+
}
|
|
162
171
|
// Security headers (helmet) - enabled by default for security
|
|
163
172
|
// Set HELMET_ENABLED=false to disable (not recommended)
|
|
164
173
|
const helmetEnabled = process.env.HELMET_ENABLED !== 'false';
|
|
@@ -419,6 +428,8 @@ export class ArchiCoreServer {
|
|
|
419
428
|
});
|
|
420
429
|
}
|
|
421
430
|
async stop() {
|
|
431
|
+
// Cleanup intervals
|
|
432
|
+
cleanupAuthIntervals();
|
|
422
433
|
// Disconnect cache and database
|
|
423
434
|
await cache.disconnect();
|
|
424
435
|
await db.close();
|
|
@@ -438,9 +449,43 @@ export class ArchiCoreServer {
|
|
|
438
449
|
return this.app;
|
|
439
450
|
}
|
|
440
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Validate required environment variables for production
|
|
454
|
+
*/
|
|
455
|
+
function validateProductionEnvironment() {
|
|
456
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
457
|
+
return; // Only enforce in production
|
|
458
|
+
}
|
|
459
|
+
const requiredVars = [
|
|
460
|
+
'JWT_SECRET',
|
|
461
|
+
'ENCRYPTION_KEY',
|
|
462
|
+
'ENCRYPTION_SALT',
|
|
463
|
+
];
|
|
464
|
+
const missingVars = [];
|
|
465
|
+
for (const varName of requiredVars) {
|
|
466
|
+
if (!process.env[varName]) {
|
|
467
|
+
missingVars.push(varName);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Check for weak JWT_SECRET
|
|
471
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
472
|
+
if (jwtSecret && jwtSecret.length < 32) {
|
|
473
|
+
Logger.error('CRITICAL: JWT_SECRET must be at least 32 characters in production');
|
|
474
|
+
missingVars.push('JWT_SECRET (too short)');
|
|
475
|
+
}
|
|
476
|
+
if (missingVars.length > 0) {
|
|
477
|
+
Logger.error('CRITICAL: Missing required environment variables for production:');
|
|
478
|
+
missingVars.forEach(v => Logger.error(` - ${v}`));
|
|
479
|
+
Logger.error('Server cannot start in production mode without these variables.');
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
Logger.info('Production environment validation passed');
|
|
483
|
+
}
|
|
441
484
|
// Запуск сервера при прямом вызове
|
|
442
485
|
const isMainModule = process.argv[1]?.includes('server');
|
|
443
486
|
if (isMainModule) {
|
|
487
|
+
// Validate production environment before starting
|
|
488
|
+
validateProductionEnvironment();
|
|
444
489
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
445
490
|
const host = process.env.HOST || '0.0.0.0';
|
|
446
491
|
const server = new ArchiCoreServer({
|
|
@@ -458,5 +503,27 @@ if (isMainModule) {
|
|
|
458
503
|
await server.stop();
|
|
459
504
|
process.exit(0);
|
|
460
505
|
});
|
|
506
|
+
process.on('SIGTERM', async () => {
|
|
507
|
+
Logger.info('SIGTERM received, shutting down gracefully...');
|
|
508
|
+
await server.stop();
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
// Handle uncaught exceptions - log and exit gracefully
|
|
512
|
+
process.on('uncaughtException', async (error) => {
|
|
513
|
+
Logger.error('Uncaught Exception:', error);
|
|
514
|
+
try {
|
|
515
|
+
await server.stop();
|
|
516
|
+
}
|
|
517
|
+
catch (stopError) {
|
|
518
|
+
Logger.error('Error during shutdown after uncaughtException:', stopError);
|
|
519
|
+
}
|
|
520
|
+
process.exit(1);
|
|
521
|
+
});
|
|
522
|
+
// Handle unhandled promise rejections
|
|
523
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
524
|
+
Logger.error('Unhandled Promise Rejection:', reason);
|
|
525
|
+
Logger.error('Promise:', promise);
|
|
526
|
+
// Don't exit - just log, but track for debugging
|
|
527
|
+
});
|
|
461
528
|
}
|
|
462
529
|
//# sourceMappingURL=index.js.map
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
394
|
-
Logger.info(`Downloaded
|
|
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
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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);
|