@intranefr/superbackend 1.6.7 → 1.7.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.
Files changed (119) hide show
  1. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
  2. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
  3. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
  4. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
  5. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
  6. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
  7. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
  8. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
  9. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
  10. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
  11. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
  12. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
  13. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
  14. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
  15. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
  16. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
  17. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
  18. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
  19. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
  20. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
  21. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
  22. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
  23. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
  24. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
  25. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
  26. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
  27. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
  28. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
  29. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
  30. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
  31. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
  32. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
  33. package/.beads/config.yaml +4 -0
  34. package/.beads/issues.jsonl +8 -0
  35. package/.beads/metadata.json +4 -0
  36. package/.env.example +8 -0
  37. package/autochangelog/.env.example +36 -0
  38. package/autochangelog/README.md +412 -0
  39. package/autochangelog/config/database.js +27 -0
  40. package/autochangelog/package.json +47 -0
  41. package/autochangelog/public/landing.html +581 -0
  42. package/autochangelog/server.js +104 -0
  43. package/autochangelog/src/app.js +181 -0
  44. package/autochangelog/src/config/database.js +26 -0
  45. package/autochangelog/src/controllers/auth.js +488 -0
  46. package/autochangelog/src/controllers/changelog.js +682 -0
  47. package/autochangelog/src/controllers/project.js +580 -0
  48. package/autochangelog/src/controllers/repository.js +780 -0
  49. package/autochangelog/src/middleware/auth.js +386 -0
  50. package/autochangelog/src/models/Changelog.js +443 -0
  51. package/autochangelog/src/models/Project.js +226 -0
  52. package/autochangelog/src/models/Repository.js +366 -0
  53. package/autochangelog/src/models/User.js +223 -0
  54. package/autochangelog/src/routes/auth.routes.js +32 -0
  55. package/autochangelog/src/routes/changelog.routes.js +42 -0
  56. package/autochangelog/src/routes/github-auth.routes.js +102 -0
  57. package/autochangelog/src/routes/project.routes.js +50 -0
  58. package/autochangelog/src/routes/repository.routes.js +54 -0
  59. package/autochangelog/src/services/changelog.js +722 -0
  60. package/autochangelog/src/services/github.js +243 -0
  61. package/autochangelog/utils/logger.js +77 -0
  62. package/autochangelog/views/404.ejs +18 -0
  63. package/autochangelog/views/dashboard.ejs +596 -0
  64. package/autochangelog/views/index.ejs +231 -0
  65. package/autochangelog/views/layouts/main.ejs +44 -0
  66. package/autochangelog/views/login.ejs +104 -0
  67. package/autochangelog/views/partials/footer.ejs +20 -0
  68. package/autochangelog/views/partials/navbar.ejs +51 -0
  69. package/autochangelog/views/register.ejs +109 -0
  70. package/autochangelog-cli/README.md +266 -0
  71. package/autochangelog-cli/bin/autochangelog +120 -0
  72. package/autochangelog-cli/package.json +46 -0
  73. package/autochangelog-cli/src/cli/commands/auth.js +291 -0
  74. package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
  75. package/autochangelog-cli/src/cli/commands/project.js +427 -0
  76. package/autochangelog-cli/src/cli/commands/repo.js +557 -0
  77. package/autochangelog-cli/src/cli/commands/stats.js +706 -0
  78. package/autochangelog-cli/src/cli/utils/config.js +277 -0
  79. package/autochangelog-cli/src/cli/utils/errors.js +307 -0
  80. package/autochangelog-cli/src/cli/utils/logger.js +75 -0
  81. package/autochangelog-cli/src/cli/utils/output.js +357 -0
  82. package/package.json +9 -3
  83. package/plugins/supercli/README.md +108 -0
  84. package/plugins/supercli/plugin.json +123 -0
  85. package/server.js +1 -1
  86. package/src/cli/api.js +380 -0
  87. package/src/cli/direct/agent-utils.js +61 -0
  88. package/src/cli/direct/cli-utils.js +112 -0
  89. package/src/cli/direct/data-seeding.js +307 -0
  90. package/src/cli/direct/db-admin.js +84 -0
  91. package/src/cli/direct/db-advanced.js +372 -0
  92. package/src/cli/direct/db-utils.js +558 -0
  93. package/src/cli/direct/help.js +195 -0
  94. package/src/cli/direct/migration.js +107 -0
  95. package/src/cli/direct/rbac-advanced.js +132 -0
  96. package/src/cli/direct/resources-additional.js +400 -0
  97. package/src/cli/direct/resources-cms-advanced.js +173 -0
  98. package/src/cli/direct/resources-cms.js +247 -0
  99. package/src/cli/direct/resources-core.js +253 -0
  100. package/src/cli/direct/resources-execution.js +367 -0
  101. package/src/cli/direct/resources-health.js +152 -0
  102. package/src/cli/direct/resources-integrations.js +182 -0
  103. package/src/cli/direct/resources-logs.js +204 -0
  104. package/src/cli/direct/resources-org-rbac.js +187 -0
  105. package/src/cli/direct/resources-system.js +236 -0
  106. package/src/cli/direct.js +556 -0
  107. package/src/controllers/admin.controller.js +4 -0
  108. package/src/controllers/auth.controller.js +148 -1
  109. package/src/controllers/waitingList.controller.js +130 -1
  110. package/src/models/RbacRole.js +1 -1
  111. package/src/models/User.js +39 -5
  112. package/src/routes/auth.routes.js +6 -0
  113. package/src/routes/waitingList.routes.js +12 -2
  114. package/src/routes/waitingListAdmin.routes.js +3 -0
  115. package/src/services/email.service.js +1 -0
  116. package/src/services/github.service.js +255 -0
  117. package/src/services/rateLimiter.service.js +29 -1
  118. package/src/services/waitingListJson.service.js +32 -3
  119. package/views/admin-waiting-list.ejs +386 -3
@@ -0,0 +1,780 @@
1
+ const Repository = require("../models/Repository");
2
+ const Project = require("../models/Project");
3
+ const User = require("../models/User");
4
+ const GitHubService = require("../services/github");
5
+ const { checkSubscription, checkUsageLimits } = require("../middleware/auth");
6
+ const logger = require("../../utils/logger");
7
+
8
+ // Create a new repository
9
+ const createRepository = async (req, res) => {
10
+ try {
11
+ const {
12
+ projectId,
13
+ githubRepoId,
14
+ githubRepoName,
15
+ githubRepoUrl,
16
+ githubRepoOwner,
17
+ githubRepoFullName,
18
+ name,
19
+ description,
20
+ settings,
21
+ } = req.body;
22
+
23
+ // Validation
24
+ if (
25
+ !projectId ||
26
+ !githubRepoId ||
27
+ !githubRepoName ||
28
+ !githubRepoUrl ||
29
+ !githubRepoOwner ||
30
+ !githubRepoFullName
31
+ ) {
32
+ return res.status(400).json({
33
+ success: false,
34
+ message: "Project ID and GitHub repository information are required.",
35
+ });
36
+ }
37
+
38
+ // Check if project exists and user owns it
39
+ const project = await Project.findById(projectId);
40
+ if (!project) {
41
+ return res.status(404).json({
42
+ success: false,
43
+ message: "Project not found.",
44
+ });
45
+ }
46
+
47
+ if (project.ownerId.toString() !== req.user._id.toString()) {
48
+ return res.status(403).json({
49
+ success: false,
50
+ message: "Access denied. You do not own this project.",
51
+ });
52
+ }
53
+
54
+ // Check if user can create more repositories
55
+ const user = await User.findById(req.user._id);
56
+ const currentRepositories = await Repository.countDocuments({
57
+ projectId,
58
+ status: "active",
59
+ });
60
+
61
+ if (currentRepositories >= user.subscription.repositoriesLimit) {
62
+ return res.status(403).json({
63
+ success: false,
64
+ message: `You have reached your repository limit of ${user.subscription.repositoriesLimit}. Upgrade your plan to connect more repositories.`,
65
+ });
66
+ }
67
+
68
+ // Check if repository already exists for this project
69
+ const existingRepo = await Repository.existsForProject(
70
+ projectId,
71
+ githubRepoId,
72
+ );
73
+ if (existingRepo) {
74
+ return res.status(400).json({
75
+ success: false,
76
+ message: "This repository is already connected to this project.",
77
+ });
78
+ }
79
+
80
+ // Validate GitHub repository access
81
+ const userWithGithub = await User.findById(req.user._id);
82
+ if (!userWithGithub.githubAccessToken) {
83
+ return res.status(400).json({
84
+ success: false,
85
+ message:
86
+ "GitHub access token not found. Please connect your GitHub account first.",
87
+ });
88
+ }
89
+
90
+ const validation = await GitHubService.validateRepositoryAccess(
91
+ userWithGithub.githubAccessToken,
92
+ githubRepoOwner,
93
+ githubRepoName,
94
+ );
95
+
96
+ if (!validation.valid) {
97
+ return res.status(400).json({
98
+ success: false,
99
+ message: `Failed to access repository: ${validation.error}`,
100
+ });
101
+ }
102
+
103
+ // Create repository
104
+ const repository = new Repository({
105
+ projectId,
106
+ githubRepoId,
107
+ githubRepoName,
108
+ githubRepoUrl,
109
+ githubRepoOwner,
110
+ githubRepoFullName,
111
+ name: name || githubRepoName,
112
+ description: description || "",
113
+ githubAccessToken: userWithGithub.githubAccessToken,
114
+ githubRefreshToken: userWithGithub.githubRefreshToken,
115
+ defaultBranch: settings?.defaultBranch || "main",
116
+ includeCommitsWithoutPR: settings?.includeCommitsWithoutPR !== false,
117
+ includeMergeCommits: settings?.includeMergeCommits || false,
118
+ includeDraftPRs: settings?.includeDraftPRs || false,
119
+ includeClosedPRs: settings?.includeClosedPRs !== false,
120
+ features: {
121
+ issues: settings?.features?.issues !== false,
122
+ pullRequests: settings?.features?.pullRequests !== false,
123
+ releases: settings?.features?.releases !== false,
124
+ discussions: settings?.features?.discussions || false,
125
+ },
126
+ });
127
+
128
+ await repository.save();
129
+
130
+ // Update project's repository count
131
+ await project.updateRepositoryCount();
132
+
133
+ // Update user's usage count
134
+ await User.findByIdAndUpdate(req.user._id, {
135
+ $inc: { "usage.repositoriesCount": 1 },
136
+ });
137
+
138
+ // Populate project info
139
+ await repository.populate("projectId", "name slug");
140
+
141
+ logger.info(
142
+ `Repository created: ${repository.name} for project ${project.name} by user ${req.user.email}`,
143
+ );
144
+
145
+ res.status(201).json({
146
+ success: true,
147
+ message: "Repository created successfully.",
148
+ data: {
149
+ repository: repository.toJSON(),
150
+ },
151
+ });
152
+ } catch (error) {
153
+ logger.error("Create repository error:", error);
154
+ res.status(500).json({
155
+ success: false,
156
+ message: "Internal server error.",
157
+ });
158
+ }
159
+ };
160
+
161
+ // Get all repositories for a project
162
+ const getRepositories = async (req, res) => {
163
+ try {
164
+ const { projectId } = req.params;
165
+ const { limit = 20, skip = 0, status = "active" } = req.query;
166
+
167
+ // Check if project exists and user owns it
168
+ const project = await Project.findById(projectId);
169
+ if (!project) {
170
+ return res.status(404).json({
171
+ success: false,
172
+ message: "Project not found.",
173
+ });
174
+ }
175
+
176
+ if (project.ownerId.toString() !== req.user._id.toString()) {
177
+ return res.status(403).json({
178
+ success: false,
179
+ message: "Access denied. You do not own this project.",
180
+ });
181
+ }
182
+
183
+ const repositories = await Repository.findByProject(projectId, {
184
+ status,
185
+ limit: parseInt(limit),
186
+ skip: parseInt(skip),
187
+ });
188
+
189
+ res.json({
190
+ success: true,
191
+ data: {
192
+ repositories: repositories.map((r) => r.toJSON()),
193
+ pagination: {
194
+ limit: parseInt(limit),
195
+ skip: parseInt(skip),
196
+ total: repositories.length,
197
+ },
198
+ },
199
+ });
200
+ } catch (error) {
201
+ logger.error("Get repositories error:", error);
202
+ res.status(500).json({
203
+ success: false,
204
+ message: "Internal server error.",
205
+ });
206
+ }
207
+ };
208
+
209
+ // Get a specific repository by ID
210
+ const getRepository = async (req, res) => {
211
+ try {
212
+ const { id } = req.params;
213
+
214
+ const repository = await Repository.findById(id).populate(
215
+ "projectId",
216
+ "name slug",
217
+ );
218
+ if (!repository) {
219
+ return res.status(404).json({
220
+ success: false,
221
+ message: "Repository not found.",
222
+ });
223
+ }
224
+
225
+ // Check ownership through project
226
+ const project = await Project.findById(repository.projectId);
227
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
228
+ return res.status(403).json({
229
+ success: false,
230
+ message: "Access denied. You do not own this repository.",
231
+ });
232
+ }
233
+
234
+ const stats = await repository.getStatistics();
235
+
236
+ res.json({
237
+ success: true,
238
+ data: {
239
+ repository: {
240
+ ...repository.toJSON(),
241
+ statistics: stats,
242
+ },
243
+ },
244
+ });
245
+ } catch (error) {
246
+ logger.error("Get repository error:", error);
247
+ res.status(500).json({
248
+ success: false,
249
+ message: "Internal server error.",
250
+ });
251
+ }
252
+ };
253
+
254
+ // Update a repository
255
+ const updateRepository = async (req, res) => {
256
+ try {
257
+ const { id } = req.params;
258
+ const { name, description, settings, tags, isPrivate } = req.body;
259
+
260
+ const repository = await Repository.findById(id);
261
+ if (!repository) {
262
+ return res.status(404).json({
263
+ success: false,
264
+ message: "Repository not found.",
265
+ });
266
+ }
267
+
268
+ // Check ownership through project
269
+ const project = await Project.findById(repository.projectId);
270
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
271
+ return res.status(403).json({
272
+ success: false,
273
+ message: "Access denied. You do not own this repository.",
274
+ });
275
+ }
276
+
277
+ // Update fields
278
+ if (name) repository.name = name.trim();
279
+ if (description !== undefined) repository.description = description.trim();
280
+ if (tags !== undefined)
281
+ repository.tags = tags.map((tag) => tag.toLowerCase().trim());
282
+ if (isPrivate !== undefined) repository.isPrivate = isPrivate;
283
+
284
+ if (settings) {
285
+ if (settings.defaultBranch)
286
+ repository.defaultBranch = settings.defaultBranch;
287
+ if (settings.includeCommitsWithoutPR !== undefined)
288
+ repository.includeCommitsWithoutPR = settings.includeCommitsWithoutPR;
289
+ if (settings.includeMergeCommits !== undefined)
290
+ repository.includeMergeCommits = settings.includeMergeCommits;
291
+ if (settings.includeDraftPRs !== undefined)
292
+ repository.includeDraftPRs = settings.includeDraftPRs;
293
+ if (settings.includeClosedPRs !== undefined)
294
+ repository.includeClosedPRs = settings.includeClosedPRs;
295
+
296
+ if (settings.features) {
297
+ repository.features = { ...repository.features, ...settings.features };
298
+ }
299
+ }
300
+
301
+ await repository.save();
302
+
303
+ logger.info(
304
+ `Repository updated: ${repository.name} by user ${req.user.email}`,
305
+ );
306
+
307
+ res.json({
308
+ success: true,
309
+ message: "Repository updated successfully.",
310
+ data: {
311
+ repository: repository.toJSON(),
312
+ },
313
+ });
314
+ } catch (error) {
315
+ logger.error("Update repository error:", error);
316
+ res.status(500).json({
317
+ success: false,
318
+ message: "Internal server error.",
319
+ });
320
+ }
321
+ };
322
+
323
+ // Delete a repository (soft delete)
324
+ const deleteRepository = async (req, res) => {
325
+ try {
326
+ const { id } = req.params;
327
+
328
+ const repository = await Repository.findById(id);
329
+ if (!repository) {
330
+ return res.status(404).json({
331
+ success: false,
332
+ message: "Repository not found.",
333
+ });
334
+ }
335
+
336
+ // Check ownership through project
337
+ const project = await Project.findById(repository.projectId);
338
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
339
+ return res.status(403).json({
340
+ success: false,
341
+ message: "Access denied. You do not own this repository.",
342
+ });
343
+ }
344
+
345
+ // Soft delete - set status to 'deleted'
346
+ repository.status = "deleted";
347
+ await repository.save();
348
+
349
+ // Update project's repository count
350
+ await project.updateRepositoryCount();
351
+
352
+ // Update user's usage count
353
+
354
+ await User.findByIdAndUpdate(req.user._id, {
355
+ $inc: { "usage.repositoriesCount": -1 },
356
+ });
357
+
358
+ logger.info(
359
+ `Repository deleted: ${repository.name} by user ${req.user.email}`,
360
+ );
361
+
362
+ res.json({
363
+ success: true,
364
+ message: "Repository deleted successfully.",
365
+ });
366
+ } catch (error) {
367
+ logger.error("Delete repository error:", error);
368
+ res.status(500).json({
369
+ success: false,
370
+ message: "Internal server error.",
371
+ });
372
+ }
373
+ };
374
+
375
+ // Sync repository data from GitHub
376
+ const syncRepository = async (req, res) => {
377
+ try {
378
+ const { id } = req.params;
379
+
380
+ const repository = await Repository.findById(id);
381
+ if (!repository) {
382
+ return res.status(404).json({
383
+ success: false,
384
+ message: "Repository not found.",
385
+ });
386
+ }
387
+
388
+ // Check ownership through project
389
+ const project = await Project.findById(repository.projectId);
390
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
391
+ return res.status(403).json({
392
+ success: false,
393
+ message: "Access denied. You do not own this repository.",
394
+ });
395
+ }
396
+
397
+ // Update sync status to in_progress
398
+ await repository.updateSyncStatus("partial");
399
+
400
+ try {
401
+ // Get repository information from GitHub
402
+ const githubRepo = await GitHubService.getRepository(
403
+ repository.githubAccessToken,
404
+ repository.githubRepoOwner,
405
+ repository.githubRepoName,
406
+ );
407
+
408
+ // Update repository metadata
409
+ await repository.updateMetadata({
410
+ totalCommits: githubRepo.size, // Approximate
411
+ totalPRs: 0, // Would need to fetch PRs
412
+ totalIssues: githubRepo.open_issues_count,
413
+ lastCommitSha: null, // Would need to fetch latest commit
414
+ lastCommitDate: new Date(),
415
+ });
416
+
417
+ // Update sync status to success
418
+ await repository.updateSyncStatus("success");
419
+
420
+ logger.info(
421
+ `Repository synced: ${repository.name} by user ${req.user.email}`,
422
+ );
423
+
424
+ res.json({
425
+ success: true,
426
+ message: "Repository synced successfully.",
427
+ data: {
428
+ repository: repository.toJSON(),
429
+ },
430
+ });
431
+ } catch (syncError) {
432
+ // Update sync status to failed
433
+ await repository.updateSyncStatus("failed", syncError.message);
434
+
435
+ logger.error(`Repository sync failed: ${repository.name}`, syncError);
436
+
437
+ res.status(500).json({
438
+ success: false,
439
+ message: "Failed to sync repository data from GitHub.",
440
+ error: syncError.message,
441
+ });
442
+ }
443
+ } catch (error) {
444
+ logger.error("Sync repository error:", error);
445
+ res.status(500).json({
446
+ success: false,
447
+ message: "Internal server error.",
448
+ });
449
+ }
450
+ };
451
+
452
+ // Get available months for changelog generation
453
+ const getAvailableMonths = async (req, res) => {
454
+ try {
455
+ const { id } = req.params;
456
+
457
+ const repository = await Repository.findById(id);
458
+ if (!repository) {
459
+ return res.status(404).json({
460
+ success: false,
461
+ message: "Repository not found.",
462
+ });
463
+ }
464
+
465
+ // Check ownership through project
466
+ const project = await Project.findById(repository.projectId);
467
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
468
+ return res.status(403).json({
469
+ success: false,
470
+ message: "Access denied. You do not own this repository.",
471
+ });
472
+ }
473
+
474
+ const availableMonths = await repository.getAvailableMonths();
475
+
476
+ res.json({
477
+ success: true,
478
+ data: {
479
+ availableMonths,
480
+ },
481
+ });
482
+ } catch (error) {
483
+ logger.error("Get available months error:", error);
484
+ res.status(500).json({
485
+ success: false,
486
+ message: "Internal server error.",
487
+ });
488
+ }
489
+ };
490
+
491
+ // Validate repository access
492
+ const validateRepository = async (req, res) => {
493
+ try {
494
+ const { id } = req.params;
495
+
496
+ const repository = await Repository.findById(id);
497
+ if (!repository) {
498
+ return res.status(404).json({
499
+ success: false,
500
+ message: "Repository not found.",
501
+ });
502
+ }
503
+
504
+ // Check ownership through project
505
+ const project = await Project.findById(repository.projectId);
506
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
507
+ return res.status(403).json({
508
+ success: false,
509
+ message: "Access denied. You do not own this repository.",
510
+ });
511
+ }
512
+
513
+ const validation = await GitHubService.validateRepositoryAccess(
514
+ repository.githubAccessToken,
515
+ repository.githubRepoOwner,
516
+ repository.githubRepoName,
517
+ );
518
+
519
+ res.json({
520
+ success: true,
521
+ data: {
522
+ valid: validation.valid,
523
+ error: validation.error,
524
+ },
525
+ });
526
+ } catch (error) {
527
+ logger.error("Validate repository error:", error);
528
+ res.status(500).json({
529
+ success: false,
530
+ message: "Internal server error.",
531
+ });
532
+ }
533
+ };
534
+
535
+ // Get repository statistics
536
+ const getRepositoryStatistics = async (req, res) => {
537
+ try {
538
+ const { id } = req.params;
539
+
540
+ const repository = await Repository.findById(id);
541
+ if (!repository) {
542
+ return res.status(404).json({
543
+ success: false,
544
+ message: "Repository not found.",
545
+ });
546
+ }
547
+
548
+ // Check ownership through project
549
+ const project = await Project.findById(repository.projectId);
550
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
551
+ return res.status(403).json({
552
+ success: false,
553
+ message: "Access denied. You do not own this repository.",
554
+ });
555
+ }
556
+
557
+ const stats = await repository.getStatistics();
558
+
559
+ res.json({
560
+ success: true,
561
+ data: {
562
+ statistics: stats,
563
+ },
564
+ });
565
+ } catch (error) {
566
+ logger.error("Get repository statistics error:", error);
567
+ res.status(500).json({
568
+ success: false,
569
+ message: "Internal server error.",
570
+ });
571
+ }
572
+ };
573
+
574
+ // Get user's GitHub repositories
575
+ const getUserGithubRepositories = async (req, res) => {
576
+ try {
577
+ const { page = 1, per_page = 100, type = "owner" } = req.query;
578
+
579
+ const user = await User.findById(req.user._id);
580
+ if (!user || !user.githubAccessToken) {
581
+ return res.status(400).json({
582
+ success: false,
583
+ message:
584
+ "GitHub access token not found. Please connect your GitHub account first.",
585
+ });
586
+ }
587
+
588
+ // Instantiate GitHub Service with user's access token
589
+ const githubService = new GitHubService(user.githubAccessToken);
590
+ const reposResponse = await githubService.getUserRepos({
591
+ page: parseInt(page),
592
+ per_page: parseInt(per_page),
593
+ sort: "updated",
594
+ visibility: "all",
595
+ });
596
+
597
+ if (!reposResponse.success) {
598
+ return res.status(400).json({
599
+ success: false,
600
+ message:
601
+ reposResponse.error?.message ||
602
+ "Failed to fetch repositories from GitHub",
603
+ });
604
+ }
605
+
606
+ // Filter out repositories already connected to any of user's projects
607
+ const userProjects = await Project.find({ ownerId: req.user._id });
608
+ const connectedRepoIds = new Set();
609
+
610
+ for (const project of userProjects) {
611
+ const projectRepos = await Repository.find({
612
+ projectId: project._id,
613
+ status: { $ne: "deleted" },
614
+ });
615
+ projectRepos.forEach((repo) => connectedRepoIds.add(repo.githubRepoId));
616
+ }
617
+
618
+ const filteredRepos = reposResponse.data.filter(
619
+ (repo) => !connectedRepoIds.has(repo.id.toString()),
620
+ );
621
+
622
+ res.json({
623
+ success: true,
624
+ data: {
625
+ repositories: filteredRepos,
626
+ connectedCount: connectedRepoIds.size,
627
+ },
628
+ });
629
+ } catch (error) {
630
+ logger.error("Get user GitHub repositories error:", error);
631
+ res.status(500).json({
632
+ success: false,
633
+ message: "Internal server error.",
634
+ });
635
+ }
636
+ };
637
+
638
+ // Connect a GitHub repository to a project
639
+ const connectGithubRepository = async (req, res) => {
640
+ try {
641
+ const { projectId, repoFullName } = req.body;
642
+
643
+ // Validation
644
+ if (!projectId || !repoFullName) {
645
+ return res.status(400).json({
646
+ success: false,
647
+ message:
648
+ "Project ID and repository full name (owner/repo) are required.",
649
+ });
650
+ }
651
+
652
+ // Check if project exists and user owns it
653
+ const project = await Project.findById(projectId);
654
+ if (!project) {
655
+ return res.status(404).json({
656
+ success: false,
657
+ message: "Project not found.",
658
+ });
659
+ }
660
+
661
+ if (project.ownerId.toString() !== req.user._id.toString()) {
662
+ return res.status(403).json({
663
+ success: false,
664
+ message: "Access denied. You do not own this project.",
665
+ });
666
+ }
667
+
668
+ // Get user's GitHub access token
669
+
670
+ const user = await User.findById(req.user._id);
671
+ if (!user || !user.githubAccessToken) {
672
+ return res.status(400).json({
673
+ success: false,
674
+ message:
675
+ "GitHub access token not found. Please connect your GitHub account first.",
676
+ });
677
+ }
678
+
679
+ // Fetch repository details from GitHub
680
+ const githubService = new GitHubService(user.githubAccessToken);
681
+ const repoResponse = await githubService.getRepo(repoFullName);
682
+
683
+ if (!repoResponse.success) {
684
+ return res.status(400).json({
685
+ success: false,
686
+ message:
687
+ repoResponse.error?.message ||
688
+ "Failed to fetch repository from GitHub",
689
+ });
690
+ }
691
+
692
+ const ghRepo = repoResponse.data;
693
+
694
+ // Check if repository already exists for this project
695
+ const existingRepo = await Repository.existsForProject(
696
+ projectId,
697
+ ghRepo.id.toString(),
698
+ );
699
+ if (existingRepo) {
700
+ return res.status(400).json({
701
+ success: false,
702
+ message: "This repository is already connected to this project.",
703
+ });
704
+ }
705
+
706
+ // Check repository limit
707
+ const currentRepositories = await Repository.countDocuments({
708
+ projectId,
709
+ status: "active",
710
+ });
711
+
712
+ // Free plan allows 5 repositories per project
713
+ const repoLimit = 5;
714
+ if (currentRepositories >= repoLimit) {
715
+ return res.status(403).json({
716
+ success: false,
717
+ message: `You have reached the repository limit of ${repoLimit} for this project. Upgrade your plan to connect more repositories.`,
718
+ });
719
+ }
720
+
721
+ // Create the repository
722
+ const repository = new Repository({
723
+ projectId,
724
+ githubRepoId: ghRepo.id.toString(),
725
+ githubRepoName: ghRepo.name,
726
+ githubRepoUrl: ghRepo.html_url,
727
+ githubRepoOwner: ghRepo.owner.login,
728
+ githubRepoFullName: ghRepo.full_name,
729
+ name: ghRepo.name,
730
+ description: ghRepo.description || "",
731
+ defaultBranch: ghRepo.default_branch || "main",
732
+ githubAccessToken: user.githubAccessToken,
733
+ isPrivate: ghRepo.private,
734
+ metadata: {
735
+ totalCommits: 0,
736
+ totalPRs: 0,
737
+ totalIssues: ghRepo.open_issues_count || 0,
738
+ },
739
+ });
740
+
741
+ await repository.save();
742
+
743
+ // Update project's repository count
744
+ await Project.findByIdAndUpdate(projectId, {
745
+ $inc: { "metadata.totalRepositories": 1 },
746
+ });
747
+
748
+ logger.info(
749
+ `Repository connected: ${repository.name} for project ${project.name} by user ${req.user.email}`,
750
+ );
751
+
752
+ res.status(201).json({
753
+ success: true,
754
+ message: "Repository connected successfully.",
755
+ data: {
756
+ repository: repository.toJSON(),
757
+ },
758
+ });
759
+ } catch (error) {
760
+ logger.error("Connect GitHub repository error:", error);
761
+ res.status(500).json({
762
+ success: false,
763
+ message: "Internal server error.",
764
+ });
765
+ }
766
+ };
767
+
768
+ module.exports = {
769
+ createRepository,
770
+ getRepositories,
771
+ getRepository,
772
+ updateRepository,
773
+ deleteRepository,
774
+ syncRepository,
775
+ getAvailableMonths,
776
+ validateRepository,
777
+ getRepositoryStatistics,
778
+ getUserGithubRepositories,
779
+ connectGithubRepository,
780
+ };