archicore 0.3.1 → 0.3.3
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/README.md +48 -4
- package/dist/cli/commands/interactive.js +83 -23
- package/dist/cli/commands/projects.js +3 -3
- package/dist/cli/ui/prompt.d.ts +4 -0
- package/dist/cli/ui/prompt.js +22 -0
- package/dist/cli/utils/config.js +2 -2
- package/dist/cli/utils/upload-utils.js +65 -18
- package/dist/code-index/ast-parser.d.ts +4 -0
- package/dist/code-index/ast-parser.js +42 -0
- package/dist/code-index/index.d.ts +21 -1
- package/dist/code-index/index.js +45 -1
- package/dist/code-index/source-map-extractor.d.ts +71 -0
- package/dist/code-index/source-map-extractor.js +194 -0
- package/dist/gitlab/gitlab-service.d.ts +162 -0
- package/dist/gitlab/gitlab-service.js +652 -0
- package/dist/gitlab/index.d.ts +8 -0
- package/dist/gitlab/index.js +8 -0
- package/dist/server/config/passport.d.ts +14 -0
- package/dist/server/config/passport.js +86 -0
- package/dist/server/index.js +52 -10
- package/dist/server/middleware/api-auth.d.ts +2 -2
- package/dist/server/middleware/api-auth.js +21 -2
- package/dist/server/middleware/csrf.d.ts +23 -0
- package/dist/server/middleware/csrf.js +96 -0
- package/dist/server/routes/auth.d.ts +2 -2
- package/dist/server/routes/auth.js +204 -5
- package/dist/server/routes/device-auth.js +2 -2
- package/dist/server/routes/gitlab.d.ts +12 -0
- package/dist/server/routes/gitlab.js +528 -0
- package/dist/server/routes/oauth.d.ts +6 -0
- package/dist/server/routes/oauth.js +198 -0
- package/dist/server/services/audit-service.d.ts +1 -1
- package/dist/server/services/auth-service.d.ts +13 -1
- package/dist/server/services/auth-service.js +108 -7
- package/dist/server/services/email-service.d.ts +63 -0
- package/dist/server/services/email-service.js +586 -0
- package/dist/server/utils/disposable-email-domains.d.ts +14 -0
- package/dist/server/utils/disposable-email-domains.js +192 -0
- package/dist/types/api.d.ts +98 -0
- package/dist/types/gitlab.d.ts +245 -0
- package/dist/types/gitlab.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab Integration Service
|
|
3
|
+
*
|
|
4
|
+
* Manages GitLab connections with support for:
|
|
5
|
+
* - Multiple GitLab instances (self-hosted + gitlab.com)
|
|
6
|
+
* - Personal Access Token (PAT) authentication
|
|
7
|
+
* - Private repositories access
|
|
8
|
+
* - Branch listing and selection
|
|
9
|
+
* - Webhooks for auto-analysis
|
|
10
|
+
* - Merge Request analysis
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
|
|
13
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
17
|
+
import { Logger } from '../utils/logger.js';
|
|
18
|
+
// Storage paths
|
|
19
|
+
const DATA_DIR = process.env.ARCHICORE_DATA_DIR || '.archicore';
|
|
20
|
+
const INSTANCES_FILE = 'gitlab-instances.json';
|
|
21
|
+
const REPOSITORIES_FILE = 'gitlab-repositories.json';
|
|
22
|
+
// Encryption key for tokens
|
|
23
|
+
const ENCRYPTION_KEY = process.env.GITLAB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY || 'archicore-gitlab-key-32bytes!!';
|
|
24
|
+
// Webhook base URL - construct from PUBLIC_URL or API_URL
|
|
25
|
+
const getWebhookUrl = () => {
|
|
26
|
+
// Use explicit GitLab webhook URL if set
|
|
27
|
+
if (process.env.GITLAB_WEBHOOK_URL) {
|
|
28
|
+
return process.env.GITLAB_WEBHOOK_URL;
|
|
29
|
+
}
|
|
30
|
+
// Otherwise construct from API_URL or PUBLIC_URL
|
|
31
|
+
const baseUrl = process.env.API_URL || process.env.PUBLIC_URL || 'http://localhost:3000';
|
|
32
|
+
return `${baseUrl}/api/gitlab/webhook`;
|
|
33
|
+
};
|
|
34
|
+
const WEBHOOK_BASE_URL = getWebhookUrl();
|
|
35
|
+
// Singleton instance
|
|
36
|
+
let instance = null;
|
|
37
|
+
export class GitLabService {
|
|
38
|
+
instances = [];
|
|
39
|
+
repositories = [];
|
|
40
|
+
initialized = false;
|
|
41
|
+
dataDir = DATA_DIR;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
// Singleton pattern
|
|
44
|
+
if (instance) {
|
|
45
|
+
return instance;
|
|
46
|
+
}
|
|
47
|
+
instance = this;
|
|
48
|
+
this.dataDir = config?.dataDir || DATA_DIR;
|
|
49
|
+
}
|
|
50
|
+
// ===== INITIALIZATION =====
|
|
51
|
+
async ensureInitialized() {
|
|
52
|
+
if (this.initialized)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
await this.loadInstances();
|
|
56
|
+
await this.loadRepositories();
|
|
57
|
+
this.initialized = true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
Logger.error('GitLab service initialization failed:', error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async loadInstances() {
|
|
65
|
+
const filePath = join(this.dataDir, INSTANCES_FILE);
|
|
66
|
+
if (existsSync(filePath)) {
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(filePath, 'utf-8');
|
|
69
|
+
const data = JSON.parse(content);
|
|
70
|
+
this.instances = data.instances || [];
|
|
71
|
+
Logger.info(`Loaded ${this.instances.length} GitLab instances`);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
Logger.warn('Could not load GitLab instances:', error);
|
|
75
|
+
this.instances = [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async saveInstances() {
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(this.dataDir)) {
|
|
82
|
+
await mkdir(this.dataDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const filePath = join(this.dataDir, INSTANCES_FILE);
|
|
85
|
+
await writeFile(filePath, JSON.stringify({ instances: this.instances }, null, 2));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
Logger.error('Could not save GitLab instances:', error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async loadRepositories() {
|
|
92
|
+
const filePath = join(this.dataDir, REPOSITORIES_FILE);
|
|
93
|
+
if (existsSync(filePath)) {
|
|
94
|
+
try {
|
|
95
|
+
const content = await readFile(filePath, 'utf-8');
|
|
96
|
+
const data = JSON.parse(content);
|
|
97
|
+
this.repositories = data.repositories || [];
|
|
98
|
+
Logger.info(`Loaded ${this.repositories.length} connected GitLab repositories`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
Logger.warn('Could not load GitLab repositories:', error);
|
|
102
|
+
this.repositories = [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async saveRepositories() {
|
|
107
|
+
try {
|
|
108
|
+
if (!existsSync(this.dataDir)) {
|
|
109
|
+
await mkdir(this.dataDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
const filePath = join(this.dataDir, REPOSITORIES_FILE);
|
|
112
|
+
await writeFile(filePath, JSON.stringify({ repositories: this.repositories }, null, 2));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
Logger.error('Could not save GitLab repositories:', error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ===== ENCRYPTION =====
|
|
119
|
+
encrypt(text) {
|
|
120
|
+
const iv = randomBytes(16);
|
|
121
|
+
const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
|
|
122
|
+
const cipher = createCipheriv('aes-256-cbc', key, iv);
|
|
123
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
124
|
+
encrypted += cipher.final('hex');
|
|
125
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
126
|
+
}
|
|
127
|
+
decrypt(encrypted) {
|
|
128
|
+
try {
|
|
129
|
+
const parts = encrypted.split(':');
|
|
130
|
+
if (parts.length !== 2)
|
|
131
|
+
return encrypted; // Not encrypted
|
|
132
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
133
|
+
const key = createHash('sha256').update(ENCRYPTION_KEY).digest();
|
|
134
|
+
const decipher = createDecipheriv('aes-256-cbc', key, iv);
|
|
135
|
+
let decrypted = decipher.update(parts[1], 'hex', 'utf8');
|
|
136
|
+
decrypted += decipher.final('utf8');
|
|
137
|
+
return decrypted;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return encrypted; // Return as-is if decryption fails
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ===== INSTANCE MANAGEMENT =====
|
|
144
|
+
/**
|
|
145
|
+
* Add a new GitLab instance connection
|
|
146
|
+
*/
|
|
147
|
+
async addInstance(userId, instanceUrl, accessToken, options = {}) {
|
|
148
|
+
await this.ensureInitialized();
|
|
149
|
+
// Normalize URL (remove trailing slash)
|
|
150
|
+
const normalizedUrl = instanceUrl.replace(/\/+$/, '');
|
|
151
|
+
// Validate token by fetching user info
|
|
152
|
+
const user = await this.fetchGitLabUser(normalizedUrl, accessToken, options.rejectUnauthorizedSSL);
|
|
153
|
+
// Check if instance already exists for this user
|
|
154
|
+
const existingIndex = this.instances.findIndex(i => i.userId === userId && i.instanceUrl === normalizedUrl);
|
|
155
|
+
const instanceData = {
|
|
156
|
+
id: existingIndex >= 0 ? this.instances[existingIndex].id : uuidv4(),
|
|
157
|
+
name: options.name || this.getInstanceName(normalizedUrl),
|
|
158
|
+
instanceUrl: normalizedUrl,
|
|
159
|
+
userId,
|
|
160
|
+
gitlabUserId: user.id,
|
|
161
|
+
gitlabUsername: user.username,
|
|
162
|
+
accessToken: this.encrypt(accessToken),
|
|
163
|
+
tokenScopes: await this.getTokenScopes(normalizedUrl, accessToken, options.rejectUnauthorizedSSL),
|
|
164
|
+
rejectUnauthorizedSSL: options.rejectUnauthorizedSSL ?? true,
|
|
165
|
+
createdAt: existingIndex >= 0 ? this.instances[existingIndex].createdAt : new Date(),
|
|
166
|
+
lastUsedAt: new Date()
|
|
167
|
+
};
|
|
168
|
+
if (existingIndex >= 0) {
|
|
169
|
+
this.instances[existingIndex] = instanceData;
|
|
170
|
+
Logger.info(`Updated GitLab instance: ${instanceData.name} for user ${userId}`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.instances.push(instanceData);
|
|
174
|
+
Logger.info(`Added GitLab instance: ${instanceData.name} for user ${userId}`);
|
|
175
|
+
}
|
|
176
|
+
await this.saveInstances();
|
|
177
|
+
return instanceData;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get instance display name from URL
|
|
181
|
+
*/
|
|
182
|
+
getInstanceName(url) {
|
|
183
|
+
try {
|
|
184
|
+
const hostname = new URL(url).hostname;
|
|
185
|
+
if (hostname === 'gitlab.com')
|
|
186
|
+
return 'GitLab.com';
|
|
187
|
+
return hostname;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return url;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get all GitLab instances for a user
|
|
195
|
+
*/
|
|
196
|
+
async getInstances(userId) {
|
|
197
|
+
await this.ensureInitialized();
|
|
198
|
+
return this.instances
|
|
199
|
+
.filter(i => i.userId === userId)
|
|
200
|
+
.map(i => ({ ...i, accessToken: '[ENCRYPTED]' })); // Don't expose tokens
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get a specific instance by ID
|
|
204
|
+
*/
|
|
205
|
+
async getInstance(instanceId) {
|
|
206
|
+
await this.ensureInitialized();
|
|
207
|
+
return this.instances.find(i => i.id === instanceId) || null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Remove a GitLab instance connection
|
|
211
|
+
*/
|
|
212
|
+
async removeInstance(userId, instanceId) {
|
|
213
|
+
await this.ensureInitialized();
|
|
214
|
+
const index = this.instances.findIndex(i => i.id === instanceId && i.userId === userId);
|
|
215
|
+
if (index === -1)
|
|
216
|
+
return false;
|
|
217
|
+
// Also remove all connected repositories for this instance
|
|
218
|
+
this.repositories = this.repositories.filter(r => r.instanceId !== instanceId);
|
|
219
|
+
this.instances.splice(index, 1);
|
|
220
|
+
await this.saveInstances();
|
|
221
|
+
await this.saveRepositories();
|
|
222
|
+
Logger.info(`Removed GitLab instance: ${instanceId}`);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
// ===== GITLAB API HELPERS =====
|
|
226
|
+
/**
|
|
227
|
+
* Make authenticated request to GitLab API
|
|
228
|
+
*/
|
|
229
|
+
async gitlabFetch(instanceUrl, accessToken, endpoint, options = {}) {
|
|
230
|
+
const url = `${instanceUrl}/api/v4${endpoint}`;
|
|
231
|
+
const fetchOptions = {
|
|
232
|
+
method: options.method || 'GET',
|
|
233
|
+
headers: {
|
|
234
|
+
'PRIVATE-TOKEN': accessToken,
|
|
235
|
+
'Content-Type': 'application/json',
|
|
236
|
+
'Accept': 'application/json'
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
if (options.body) {
|
|
240
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
241
|
+
}
|
|
242
|
+
// Note: For self-signed certificates, Node.js fetch doesn't support
|
|
243
|
+
// rejectUnauthorized directly. Users may need to set NODE_TLS_REJECT_UNAUTHORIZED=0
|
|
244
|
+
// or use a custom agent in production.
|
|
245
|
+
const response = await fetch(url, fetchOptions);
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
const errorText = await response.text();
|
|
248
|
+
let errorMessage = `GitLab API error: ${response.status}`;
|
|
249
|
+
try {
|
|
250
|
+
const errorJson = JSON.parse(errorText);
|
|
251
|
+
errorMessage = errorJson.message || errorJson.error || errorMessage;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
errorMessage = errorText || errorMessage;
|
|
255
|
+
}
|
|
256
|
+
throw new Error(errorMessage);
|
|
257
|
+
}
|
|
258
|
+
return response.json();
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Fetch current GitLab user info
|
|
262
|
+
*/
|
|
263
|
+
async fetchGitLabUser(instanceUrl, accessToken, rejectUnauthorizedSSL) {
|
|
264
|
+
return this.gitlabFetch(instanceUrl, accessToken, '/user', { rejectUnauthorizedSSL });
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get token scopes (by testing API endpoints)
|
|
268
|
+
*/
|
|
269
|
+
async getTokenScopes(instanceUrl, accessToken, rejectUnauthorizedSSL) {
|
|
270
|
+
const scopes = [];
|
|
271
|
+
// Test read_user scope
|
|
272
|
+
try {
|
|
273
|
+
await this.gitlabFetch(instanceUrl, accessToken, '/user', { rejectUnauthorizedSSL });
|
|
274
|
+
scopes.push('read_user');
|
|
275
|
+
}
|
|
276
|
+
catch { /* no access */ }
|
|
277
|
+
// Test read_api/api scope (list projects)
|
|
278
|
+
try {
|
|
279
|
+
await this.gitlabFetch(instanceUrl, accessToken, '/projects?per_page=1', { rejectUnauthorizedSSL });
|
|
280
|
+
scopes.push('read_api');
|
|
281
|
+
}
|
|
282
|
+
catch { /* no access */ }
|
|
283
|
+
// Test read_repository scope
|
|
284
|
+
try {
|
|
285
|
+
const projects = await this.gitlabFetch(instanceUrl, accessToken, '/projects?per_page=1&membership=true', { rejectUnauthorizedSSL });
|
|
286
|
+
if (projects.length > 0) {
|
|
287
|
+
try {
|
|
288
|
+
await this.gitlabFetch(instanceUrl, accessToken, `/projects/${projects[0].id}/repository/branches?per_page=1`, { rejectUnauthorizedSSL });
|
|
289
|
+
scopes.push('read_repository');
|
|
290
|
+
}
|
|
291
|
+
catch { /* no access */ }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch { /* no access */ }
|
|
295
|
+
// Test write_repository scope (we don't actually write, just assume if api scope)
|
|
296
|
+
if (scopes.includes('read_api')) {
|
|
297
|
+
scopes.push('api'); // Full API access likely
|
|
298
|
+
}
|
|
299
|
+
return scopes;
|
|
300
|
+
}
|
|
301
|
+
// ===== REPOSITORY MANAGEMENT =====
|
|
302
|
+
/**
|
|
303
|
+
* List all accessible projects from a GitLab instance
|
|
304
|
+
*/
|
|
305
|
+
async listProjects(userId, instanceId, options = {}) {
|
|
306
|
+
await this.ensureInitialized();
|
|
307
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
308
|
+
if (!instance) {
|
|
309
|
+
throw new Error('GitLab instance not found');
|
|
310
|
+
}
|
|
311
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
312
|
+
const params = new URLSearchParams();
|
|
313
|
+
if (options.search)
|
|
314
|
+
params.set('search', options.search);
|
|
315
|
+
if (options.membership !== false)
|
|
316
|
+
params.set('membership', 'true');
|
|
317
|
+
if (options.owned)
|
|
318
|
+
params.set('owned', 'true');
|
|
319
|
+
params.set('order_by', 'last_activity_at');
|
|
320
|
+
params.set('sort', 'desc');
|
|
321
|
+
params.set('per_page', String(options.perPage || 50));
|
|
322
|
+
params.set('page', String(options.page || 1));
|
|
323
|
+
const endpoint = `/projects?${params.toString()}`;
|
|
324
|
+
return this.gitlabFetch(instance.instanceUrl, accessToken, endpoint, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get a specific project by ID
|
|
328
|
+
*/
|
|
329
|
+
async getProject(userId, instanceId, projectId) {
|
|
330
|
+
await this.ensureInitialized();
|
|
331
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
332
|
+
if (!instance) {
|
|
333
|
+
throw new Error('GitLab instance not found');
|
|
334
|
+
}
|
|
335
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
336
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
337
|
+
return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* List branches for a project
|
|
341
|
+
*/
|
|
342
|
+
async listBranches(userId, instanceId, projectId) {
|
|
343
|
+
await this.ensureInitialized();
|
|
344
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
345
|
+
if (!instance) {
|
|
346
|
+
throw new Error('GitLab instance not found');
|
|
347
|
+
}
|
|
348
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
349
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
350
|
+
return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/repository/branches?per_page=100`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Connect a GitLab repository to ArchiCore
|
|
354
|
+
*/
|
|
355
|
+
async connectRepository(userId, instanceId, projectId, options = {}) {
|
|
356
|
+
await this.ensureInitialized();
|
|
357
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
358
|
+
if (!instance) {
|
|
359
|
+
throw new Error('GitLab instance not found');
|
|
360
|
+
}
|
|
361
|
+
// Check if already connected
|
|
362
|
+
const existing = this.repositories.find(r => r.instanceId === instanceId && r.gitlabProjectId === projectId);
|
|
363
|
+
if (existing && !options.forceReconnect) {
|
|
364
|
+
return existing;
|
|
365
|
+
}
|
|
366
|
+
// Fetch project info
|
|
367
|
+
const project = await this.getProject(userId, instanceId, projectId);
|
|
368
|
+
const repoData = {
|
|
369
|
+
id: existing?.id || uuidv4(),
|
|
370
|
+
instanceId,
|
|
371
|
+
gitlabProjectId: project.id,
|
|
372
|
+
fullPath: project.path_with_namespace,
|
|
373
|
+
name: project.name,
|
|
374
|
+
owner: project.namespace.path,
|
|
375
|
+
visibility: project.visibility,
|
|
376
|
+
defaultBranch: project.default_branch,
|
|
377
|
+
autoAnalyze: options.autoAnalyze ?? true,
|
|
378
|
+
analyzeMRs: options.analyzeMRs ?? true,
|
|
379
|
+
status: 'pending',
|
|
380
|
+
createdAt: existing?.createdAt || new Date(),
|
|
381
|
+
updatedAt: new Date()
|
|
382
|
+
};
|
|
383
|
+
// Remove existing if force reconnect
|
|
384
|
+
if (existing) {
|
|
385
|
+
const index = this.repositories.findIndex(r => r.id === existing.id);
|
|
386
|
+
if (index >= 0) {
|
|
387
|
+
this.repositories.splice(index, 1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.repositories.push(repoData);
|
|
391
|
+
await this.saveRepositories();
|
|
392
|
+
Logger.info(`Connected GitLab repository: ${project.path_with_namespace}`);
|
|
393
|
+
return repoData;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get all connected repositories for a user
|
|
397
|
+
*/
|
|
398
|
+
async getConnectedRepositories(userId) {
|
|
399
|
+
await this.ensureInitialized();
|
|
400
|
+
// Get user's instances
|
|
401
|
+
const userInstanceIds = this.instances
|
|
402
|
+
.filter(i => i.userId === userId)
|
|
403
|
+
.map(i => i.id);
|
|
404
|
+
return this.repositories.filter(r => userInstanceIds.includes(r.instanceId));
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get a specific connected repository
|
|
408
|
+
*/
|
|
409
|
+
async getConnectedRepository(repoId) {
|
|
410
|
+
await this.ensureInitialized();
|
|
411
|
+
return this.repositories.find(r => r.id === repoId) || null;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Disconnect a repository
|
|
415
|
+
*/
|
|
416
|
+
async disconnectRepository(userId, repoId) {
|
|
417
|
+
await this.ensureInitialized();
|
|
418
|
+
const repo = this.repositories.find(r => r.id === repoId);
|
|
419
|
+
if (!repo)
|
|
420
|
+
return false;
|
|
421
|
+
// Verify ownership
|
|
422
|
+
const instance = this.instances.find(i => i.id === repo.instanceId);
|
|
423
|
+
if (!instance || instance.userId !== userId)
|
|
424
|
+
return false;
|
|
425
|
+
// Remove webhook if exists
|
|
426
|
+
if (repo.webhookId) {
|
|
427
|
+
try {
|
|
428
|
+
await this.deleteWebhook(userId, repo.instanceId, repo.gitlabProjectId, repo.webhookId);
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
Logger.warn(`Failed to delete webhook for ${repo.fullPath}:`, error);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const index = this.repositories.findIndex(r => r.id === repoId);
|
|
435
|
+
if (index >= 0) {
|
|
436
|
+
this.repositories.splice(index, 1);
|
|
437
|
+
await this.saveRepositories();
|
|
438
|
+
Logger.info(`Disconnected GitLab repository: ${repo.fullPath}`);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Update repository's ArchiCore project ID
|
|
445
|
+
*/
|
|
446
|
+
async updateRepositoryProjectId(repoId, projectId) {
|
|
447
|
+
await this.ensureInitialized();
|
|
448
|
+
const repo = this.repositories.find(r => r.id === repoId);
|
|
449
|
+
if (repo) {
|
|
450
|
+
repo.projectId = projectId;
|
|
451
|
+
repo.updatedAt = new Date();
|
|
452
|
+
await this.saveRepositories();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Update repository status
|
|
457
|
+
*/
|
|
458
|
+
async updateRepositoryStatus(repoId, status, error) {
|
|
459
|
+
await this.ensureInitialized();
|
|
460
|
+
const repo = this.repositories.find(r => r.id === repoId);
|
|
461
|
+
if (repo) {
|
|
462
|
+
repo.status = status;
|
|
463
|
+
repo.lastError = error;
|
|
464
|
+
repo.updatedAt = new Date();
|
|
465
|
+
await this.saveRepositories();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Update last analyzed timestamp
|
|
470
|
+
*/
|
|
471
|
+
async updateLastAnalyzed(repoId) {
|
|
472
|
+
await this.ensureInitialized();
|
|
473
|
+
const repo = this.repositories.find(r => r.id === repoId);
|
|
474
|
+
if (repo) {
|
|
475
|
+
repo.lastAnalyzedAt = new Date();
|
|
476
|
+
repo.updatedAt = new Date();
|
|
477
|
+
await this.saveRepositories();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ===== REPOSITORY CONTENT =====
|
|
481
|
+
/**
|
|
482
|
+
* Download repository archive
|
|
483
|
+
*/
|
|
484
|
+
async downloadRepository(userId, instanceId, projectId, ref) {
|
|
485
|
+
await this.ensureInitialized();
|
|
486
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
487
|
+
if (!instance) {
|
|
488
|
+
throw new Error('GitLab instance not found');
|
|
489
|
+
}
|
|
490
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
491
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
492
|
+
const sha = ref || 'HEAD';
|
|
493
|
+
const url = `${instance.instanceUrl}/api/v4/projects/${encodedId}/repository/archive.zip?sha=${encodeURIComponent(sha)}`;
|
|
494
|
+
const response = await fetch(url, {
|
|
495
|
+
headers: {
|
|
496
|
+
'PRIVATE-TOKEN': accessToken
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
throw new Error(`Failed to download repository: ${response.status}`);
|
|
501
|
+
}
|
|
502
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
503
|
+
return Buffer.from(arrayBuffer);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get file content from repository
|
|
507
|
+
*/
|
|
508
|
+
async getFileContent(userId, instanceId, projectId, filePath, ref) {
|
|
509
|
+
await this.ensureInitialized();
|
|
510
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
511
|
+
if (!instance) {
|
|
512
|
+
throw new Error('GitLab instance not found');
|
|
513
|
+
}
|
|
514
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
515
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
516
|
+
const encodedPath = encodeURIComponent(filePath);
|
|
517
|
+
const refParam = ref ? `?ref=${encodeURIComponent(ref)}` : '';
|
|
518
|
+
const file = await this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/repository/files/${encodedPath}${refParam}`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
|
|
519
|
+
if (file.encoding === 'base64') {
|
|
520
|
+
return Buffer.from(file.content, 'base64').toString('utf-8');
|
|
521
|
+
}
|
|
522
|
+
return file.content;
|
|
523
|
+
}
|
|
524
|
+
// ===== MERGE REQUESTS =====
|
|
525
|
+
/**
|
|
526
|
+
* Get merge request details
|
|
527
|
+
*/
|
|
528
|
+
async getMergeRequest(userId, instanceId, projectId, mrIid) {
|
|
529
|
+
await this.ensureInitialized();
|
|
530
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
531
|
+
if (!instance) {
|
|
532
|
+
throw new Error('GitLab instance not found');
|
|
533
|
+
}
|
|
534
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
535
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
536
|
+
return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/merge_requests/${mrIid}`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get merge request changes (diff)
|
|
540
|
+
*/
|
|
541
|
+
async getMergeRequestChanges(userId, instanceId, projectId, mrIid) {
|
|
542
|
+
await this.ensureInitialized();
|
|
543
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
544
|
+
if (!instance) {
|
|
545
|
+
throw new Error('GitLab instance not found');
|
|
546
|
+
}
|
|
547
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
548
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
549
|
+
const response = await this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/merge_requests/${mrIid}/changes`, { rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL });
|
|
550
|
+
return response.changes;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Post a comment on merge request
|
|
554
|
+
*/
|
|
555
|
+
async postMRComment(userId, instanceId, projectId, mrIid, body) {
|
|
556
|
+
await this.ensureInitialized();
|
|
557
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
558
|
+
if (!instance) {
|
|
559
|
+
throw new Error('GitLab instance not found');
|
|
560
|
+
}
|
|
561
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
562
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
563
|
+
return this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/merge_requests/${mrIid}/notes`, {
|
|
564
|
+
method: 'POST',
|
|
565
|
+
body: { body },
|
|
566
|
+
rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// ===== WEBHOOKS =====
|
|
570
|
+
/**
|
|
571
|
+
* Create webhook for repository
|
|
572
|
+
*/
|
|
573
|
+
async createWebhook(userId, instanceId, projectId) {
|
|
574
|
+
await this.ensureInitialized();
|
|
575
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
576
|
+
if (!instance) {
|
|
577
|
+
throw new Error('GitLab instance not found');
|
|
578
|
+
}
|
|
579
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
580
|
+
const secret = randomBytes(32).toString('hex');
|
|
581
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
582
|
+
const webhook = await this.gitlabFetch(instance.instanceUrl, accessToken, `/projects/${encodedId}/hooks`, {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
body: {
|
|
585
|
+
url: WEBHOOK_BASE_URL,
|
|
586
|
+
token: secret,
|
|
587
|
+
push_events: true,
|
|
588
|
+
merge_requests_events: true,
|
|
589
|
+
enable_ssl_verification: instance.rejectUnauthorizedSSL
|
|
590
|
+
},
|
|
591
|
+
rejectUnauthorizedSSL: instance.rejectUnauthorizedSSL
|
|
592
|
+
});
|
|
593
|
+
return { id: webhook.id, secret };
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Delete webhook
|
|
597
|
+
*/
|
|
598
|
+
async deleteWebhook(userId, instanceId, projectId, hookId) {
|
|
599
|
+
await this.ensureInitialized();
|
|
600
|
+
const instance = this.instances.find(i => i.id === instanceId && i.userId === userId);
|
|
601
|
+
if (!instance) {
|
|
602
|
+
throw new Error('GitLab instance not found');
|
|
603
|
+
}
|
|
604
|
+
const accessToken = this.decrypt(instance.accessToken);
|
|
605
|
+
const encodedId = encodeURIComponent(String(projectId));
|
|
606
|
+
await fetch(`${instance.instanceUrl}/api/v4/projects/${encodedId}/hooks/${hookId}`, {
|
|
607
|
+
method: 'DELETE',
|
|
608
|
+
headers: { 'PRIVATE-TOKEN': accessToken }
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Verify webhook token
|
|
613
|
+
*/
|
|
614
|
+
verifyWebhookToken(token, expectedToken) {
|
|
615
|
+
return token === expectedToken;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Find repository by webhook (for incoming webhooks)
|
|
619
|
+
*/
|
|
620
|
+
async findRepositoryByWebhook(fullPath) {
|
|
621
|
+
await this.ensureInitialized();
|
|
622
|
+
return this.repositories.find(r => r.fullPath === fullPath) || null;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get instance for repository (used in webhook handling)
|
|
626
|
+
*/
|
|
627
|
+
async getInstanceForRepository(repo) {
|
|
628
|
+
await this.ensureInitialized();
|
|
629
|
+
return this.instances.find(i => i.id === repo.instanceId) || null;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Get decrypted webhook secret
|
|
633
|
+
*/
|
|
634
|
+
getWebhookSecret(encryptedSecret) {
|
|
635
|
+
return this.decrypt(encryptedSecret);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Disconnect repository by ArchiCore project ID
|
|
639
|
+
*/
|
|
640
|
+
async disconnectRepositoryByProjectId(projectId) {
|
|
641
|
+
await this.ensureInitialized();
|
|
642
|
+
const repo = this.repositories.find(r => r.projectId === projectId);
|
|
643
|
+
if (!repo)
|
|
644
|
+
return false;
|
|
645
|
+
const instance = this.instances.find(i => i.id === repo.instanceId);
|
|
646
|
+
if (!instance)
|
|
647
|
+
return false;
|
|
648
|
+
return this.disconnectRepository(instance.userId, repo.id);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
export default GitLabService;
|
|
652
|
+
//# sourceMappingURL=gitlab-service.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passport OAuth Configuration
|
|
3
|
+
* Google and GitHub authentication strategies
|
|
4
|
+
*/
|
|
5
|
+
import passport from 'passport';
|
|
6
|
+
export interface OAuthProfile {
|
|
7
|
+
id: string;
|
|
8
|
+
email: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
avatar?: string;
|
|
11
|
+
provider: 'google' | 'github';
|
|
12
|
+
}
|
|
13
|
+
export default passport;
|
|
14
|
+
//# sourceMappingURL=passport.d.ts.map
|