@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,682 @@
1
+ const Changelog = require("../models/Changelog");
2
+ const Project = require("../models/Project");
3
+ const Repository = require("../models/Repository");
4
+ const ChangelogService = require("../services/changelog");
5
+ const { checkSubscription, checkUsageLimits } = require("../middleware/auth");
6
+ const logger = require("../../utils/logger");
7
+
8
+ // Get User model from SuperBackend or fallback to local
9
+ const getUserModel = () => {
10
+ const sb = globalThis.superbackend || globalThis.saasbackend;
11
+ return sb?.models?.User || require("../models/User");
12
+ };
13
+
14
+ // Generate a new changelog
15
+ const generateChangelog = async (req, res) => {
16
+ try {
17
+ const {
18
+ projectId,
19
+ repositoryId,
20
+ month,
21
+ year,
22
+ format,
23
+ template,
24
+ includeCommitsWithoutPR,
25
+ } = req.body;
26
+
27
+ // Validation
28
+ if (!projectId || !repositoryId || !month || !year) {
29
+ return res.status(400).json({
30
+ success: false,
31
+ message: "Project ID, repository ID, month, and year are required.",
32
+ });
33
+ }
34
+
35
+ // Check if project and repository exist and user owns them
36
+ const [project, repository] = await Promise.all([
37
+ Project.findById(projectId),
38
+ Repository.findById(repositoryId),
39
+ ]);
40
+
41
+ if (!project || !repository) {
42
+ return res.status(404).json({
43
+ success: false,
44
+ message: "Project or repository not found.",
45
+ });
46
+ }
47
+
48
+ if (
49
+ project.ownerId.toString() !== req.user._id.toString() ||
50
+ repository.ownerId.toString() !== req.user._id.toString()
51
+ ) {
52
+ return res.status(403).json({
53
+ success: false,
54
+ message: "Access denied. You do not own this project or repository.",
55
+ });
56
+ }
57
+
58
+ // Check if repository is active
59
+ if (repository.status !== "active") {
60
+ return res.status(400).json({
61
+ success: false,
62
+ message: "Repository is not active.",
63
+ });
64
+ }
65
+
66
+ // Check if user can create more changelogs
67
+ const User = getUserModel();
68
+ const user = await User.findById(req.user._id);
69
+ const currentChangelogs = await Changelog.countDocuments({
70
+ projectId,
71
+ status: { $in: ["completed", "failed"] },
72
+ });
73
+
74
+ if (currentChangelogs >= user.subscription.changelogsLimit) {
75
+ return res.status(403).json({
76
+ success: false,
77
+ message: `You have reached your changelog limit of ${user.subscription.changelogsLimit} per month. Upgrade your plan or wait until next month.`,
78
+ });
79
+ }
80
+
81
+ // Check if changelog already exists for this period
82
+ const existingChangelog = await Changelog.existsForPeriod(
83
+ repositoryId,
84
+ month,
85
+ year,
86
+ );
87
+ if (existingChangelog) {
88
+ return res.status(400).json({
89
+ success: false,
90
+ message: "Changelog already exists for this period.",
91
+ });
92
+ }
93
+
94
+ // Generate changelog
95
+ const changelog = await ChangelogService.generateChangelog(
96
+ projectId,
97
+ repositoryId,
98
+ month,
99
+ year,
100
+ {
101
+ format: format || "markdown",
102
+ template: template || "default",
103
+ includeCommitsWithoutPR: includeCommitsWithoutPR !== false,
104
+ },
105
+ );
106
+
107
+ logger.info(
108
+ `Changelog generated: ${changelog.title} by user ${req.user.email}`,
109
+ );
110
+
111
+ res.status(201).json({
112
+ success: true,
113
+ message: "Changelog generated successfully.",
114
+ data: {
115
+ changelog: changelog.toJSON(),
116
+ },
117
+ });
118
+ } catch (error) {
119
+ logger.error("Generate changelog error:", error);
120
+ res.status(500).json({
121
+ success: false,
122
+ message: "Internal server error.",
123
+ });
124
+ }
125
+ };
126
+
127
+ // Get all changelogs for a project
128
+ const getChangelogs = async (req, res) => {
129
+ try {
130
+ const { projectId } = req.params;
131
+ const {
132
+ limit = 20,
133
+ skip = 0,
134
+ status = ["completed"],
135
+ sortBy = "createdAt",
136
+ sortOrder = -1,
137
+ } = req.query;
138
+
139
+ // Check if project exists and user owns it
140
+ const project = await Project.findById(projectId);
141
+ if (!project) {
142
+ return res.status(404).json({
143
+ success: false,
144
+ message: "Project not found.",
145
+ });
146
+ }
147
+
148
+ if (project.ownerId.toString() !== req.user._id.toString()) {
149
+ return res.status(403).json({
150
+ success: false,
151
+ message: "Access denied. You do not own this project.",
152
+ });
153
+ }
154
+
155
+ const changelogs = await Changelog.findByProject(projectId, {
156
+ status,
157
+ limit: parseInt(limit),
158
+ skip: parseInt(skip),
159
+ sortBy,
160
+ sortOrder: parseInt(sortOrder),
161
+ });
162
+
163
+ res.json({
164
+ success: true,
165
+ data: {
166
+ changelogs: changelogs.map((c) => c.toJSON()),
167
+ pagination: {
168
+ limit: parseInt(limit),
169
+ skip: parseInt(skip),
170
+ total: changelogs.length,
171
+ },
172
+ },
173
+ });
174
+ } catch (error) {
175
+ logger.error("Get changelogs error:", error);
176
+ res.status(500).json({
177
+ success: false,
178
+ message: "Internal server error.",
179
+ });
180
+ }
181
+ };
182
+
183
+ // Get all changelogs for a repository
184
+ const getRepositoryChangelogs = async (req, res) => {
185
+ try {
186
+ const { repositoryId } = req.params;
187
+ const {
188
+ limit = 20,
189
+ skip = 0,
190
+ status = ["completed"],
191
+ sortBy = "createdAt",
192
+ sortOrder = -1,
193
+ } = req.query;
194
+
195
+ // Check if repository exists and user owns it
196
+ const repository = await Repository.findById(repositoryId);
197
+ if (!repository) {
198
+ return res.status(404).json({
199
+ success: false,
200
+ message: "Repository not found.",
201
+ });
202
+ }
203
+
204
+ // Check ownership through project
205
+ const project = await Project.findById(repository.projectId);
206
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
207
+ return res.status(403).json({
208
+ success: false,
209
+ message: "Access denied. You do not own this repository.",
210
+ });
211
+ }
212
+
213
+ const changelogs = await Changelog.findByRepository(repositoryId, {
214
+ status,
215
+ limit: parseInt(limit),
216
+ skip: parseInt(skip),
217
+ sortBy,
218
+ sortOrder: parseInt(sortOrder),
219
+ });
220
+
221
+ res.json({
222
+ success: true,
223
+ data: {
224
+ changelogs: changelogs.map((c) => c.toJSON()),
225
+ pagination: {
226
+ limit: parseInt(limit),
227
+ skip: parseInt(skip),
228
+ total: changelogs.length,
229
+ },
230
+ },
231
+ });
232
+ } catch (error) {
233
+ logger.error("Get repository changelogs error:", error);
234
+ res.status(500).json({
235
+ success: false,
236
+ message: "Internal server error.",
237
+ });
238
+ }
239
+ };
240
+
241
+ // Get a specific changelog by ID
242
+ const getChangelog = async (req, res) => {
243
+ try {
244
+ const { id } = req.params;
245
+
246
+ const changelog = await Changelog.findById(id)
247
+ .populate("projectId", "name slug")
248
+ .populate("repositoryId", "name githubRepoName")
249
+ .populate("generatedBy", "name email");
250
+
251
+ if (!changelog) {
252
+ return res.status(404).json({
253
+ success: false,
254
+ message: "Changelog not found.",
255
+ });
256
+ }
257
+
258
+ // Check ownership through project
259
+ const project = await Project.findById(changelog.projectId);
260
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
261
+ return res.status(403).json({
262
+ success: false,
263
+ message: "Access denied. You do not own this changelog.",
264
+ });
265
+ }
266
+
267
+ // Increment view count for public changelogs
268
+ if (changelog.isPublic) {
269
+ await changelog.incrementViewCount();
270
+ }
271
+
272
+ res.json({
273
+ success: true,
274
+ data: {
275
+ changelog: changelog.toJSON(),
276
+ },
277
+ });
278
+ } catch (error) {
279
+ logger.error("Get changelog error:", error);
280
+ res.status(500).json({
281
+ success: false,
282
+ message: "Internal server error.",
283
+ });
284
+ }
285
+ };
286
+
287
+ // Update a changelog
288
+ const updateChangelog = async (req, res) => {
289
+ try {
290
+ const { id } = req.params;
291
+ const { title, description, format, template, isPublic, shareToken } =
292
+ req.body;
293
+
294
+ const changelog = await Changelog.findById(id);
295
+ if (!changelog) {
296
+ return res.status(404).json({
297
+ success: false,
298
+ message: "Changelog not found.",
299
+ });
300
+ }
301
+
302
+ // Check ownership through project
303
+ const project = await Project.findById(changelog.projectId);
304
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
305
+ return res.status(403).json({
306
+ success: false,
307
+ message: "Access denied. You do not own this changelog.",
308
+ });
309
+ }
310
+
311
+ // Update fields
312
+ if (title) changelog.title = title.trim();
313
+ if (description !== undefined) changelog.description = description.trim();
314
+ if (format) changelog.format = format;
315
+ if (template) changelog.template = template;
316
+ if (isPublic !== undefined) changelog.isPublic = isPublic;
317
+ if (shareToken !== undefined) changelog.shareToken = shareToken;
318
+
319
+ await changelog.save();
320
+
321
+ logger.info(
322
+ `Changelog updated: ${changelog.title} by user ${req.user.email}`,
323
+ );
324
+
325
+ res.json({
326
+ success: true,
327
+ message: "Changelog updated successfully.",
328
+ data: {
329
+ changelog: changelog.toJSON(),
330
+ },
331
+ });
332
+ } catch (error) {
333
+ logger.error("Update changelog error:", error);
334
+ res.status(500).json({
335
+ success: false,
336
+ message: "Internal server error.",
337
+ });
338
+ }
339
+ };
340
+
341
+ // Delete a changelog (soft delete)
342
+ const deleteChangelog = async (req, res) => {
343
+ try {
344
+ const { id } = req.params;
345
+
346
+ const changelog = await Changelog.findById(id);
347
+ if (!changelog) {
348
+ return res.status(404).json({
349
+ success: false,
350
+ message: "Changelog not found.",
351
+ });
352
+ }
353
+
354
+ // Check ownership through project
355
+ const project = await Project.findById(changelog.projectId);
356
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
357
+ return res.status(403).json({
358
+ success: false,
359
+ message: "Access denied. You do not own this changelog.",
360
+ });
361
+ }
362
+
363
+ // Soft delete - set status to 'deleted'
364
+ changelog.status = "deleted";
365
+ await changelog.save();
366
+
367
+ // Update user's usage count
368
+ const User = getUserModel();
369
+ await User.findByIdAndUpdate(req.user._id, {
370
+ $inc: { "usage.changelogsCount": -1 },
371
+ });
372
+
373
+ logger.info(
374
+ `Changelog deleted: ${changelog.title} by user ${req.user.email}`,
375
+ );
376
+
377
+ res.json({
378
+ success: true,
379
+ message: "Changelog deleted successfully.",
380
+ });
381
+ } catch (error) {
382
+ logger.error("Delete changelog error:", error);
383
+ res.status(500).json({
384
+ success: false,
385
+ message: "Internal server error.",
386
+ });
387
+ }
388
+ };
389
+
390
+ // Get changelog statistics
391
+ const getChangelogStatistics = async (req, res) => {
392
+ try {
393
+ const { id } = req.params;
394
+
395
+ const changelog = await Changelog.findById(id);
396
+ if (!changelog) {
397
+ return res.status(404).json({
398
+ success: false,
399
+ message: "Changelog not found.",
400
+ });
401
+ }
402
+
403
+ // Check ownership through project
404
+ const project = await Project.findById(changelog.projectId);
405
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
406
+ return res.status(403).json({
407
+ success: false,
408
+ message: "Access denied. You do not own this changelog.",
409
+ });
410
+ }
411
+
412
+ const stats = await changelog.getStatistics();
413
+
414
+ res.json({
415
+ success: true,
416
+ data: {
417
+ statistics: stats,
418
+ },
419
+ });
420
+ } catch (error) {
421
+ logger.error("Get changelog statistics error:", error);
422
+ res.status(500).json({
423
+ success: false,
424
+ message: "Internal server error.",
425
+ });
426
+ }
427
+ };
428
+
429
+ // Get available months for changelog generation
430
+ const getAvailableMonths = async (req, res) => {
431
+ try {
432
+ const { repositoryId } = req.params;
433
+
434
+ // Check if repository exists and user owns it
435
+ const repository = await Repository.findById(repositoryId);
436
+ if (!repository) {
437
+ return res.status(404).json({
438
+ success: false,
439
+ message: "Repository not found.",
440
+ });
441
+ }
442
+
443
+ // Check ownership through project
444
+ const project = await Project.findById(repository.projectId);
445
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
446
+ return res.status(403).json({
447
+ success: false,
448
+ message: "Access denied. You do not own this repository.",
449
+ });
450
+ }
451
+
452
+ const availableMonths =
453
+ await ChangelogService.getAvailableMonths(repositoryId);
454
+
455
+ res.json({
456
+ success: true,
457
+ data: {
458
+ availableMonths,
459
+ },
460
+ });
461
+ } catch (error) {
462
+ logger.error("Get available months error:", error);
463
+ res.status(500).json({
464
+ success: false,
465
+ message: "Internal server error.",
466
+ });
467
+ }
468
+ };
469
+
470
+ // Share a changelog (generate share token)
471
+ const shareChangelog = async (req, res) => {
472
+ try {
473
+ const { id } = req.params;
474
+
475
+ const changelog = await Changelog.findById(id);
476
+ if (!changelog) {
477
+ return res.status(404).json({
478
+ success: false,
479
+ message: "Changelog not found.",
480
+ });
481
+ }
482
+
483
+ // Check ownership through project
484
+ const project = await Project.findById(changelog.projectId);
485
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
486
+ return res.status(403).json({
487
+ success: false,
488
+ message: "Access denied. You do not own this changelog.",
489
+ });
490
+ }
491
+
492
+ // Make changelog public and generate share token
493
+ changelog.isPublic = true;
494
+ if (!changelog.shareToken) {
495
+ const crypto = require("crypto");
496
+ changelog.shareToken = crypto.randomBytes(16).toString("hex");
497
+ }
498
+ await changelog.save();
499
+
500
+ logger.info(
501
+ `Changelog shared: ${changelog.title} by user ${req.user.email}`,
502
+ );
503
+
504
+ res.json({
505
+ success: true,
506
+ message: "Changelog shared successfully.",
507
+ data: {
508
+ shareUrl: `${process.env.FRONTEND_URL}/share/${changelog.shareToken}`,
509
+ shareToken: changelog.shareToken,
510
+ },
511
+ });
512
+ } catch (error) {
513
+ logger.error("Share changelog error:", error);
514
+ res.status(500).json({
515
+ success: false,
516
+ message: "Internal server error.",
517
+ });
518
+ }
519
+ };
520
+
521
+ // Unshare a changelog
522
+ const unshareChangelog = async (req, res) => {
523
+ try {
524
+ const { id } = req.params;
525
+
526
+ const changelog = await Changelog.findById(id);
527
+ if (!changelog) {
528
+ return res.status(404).json({
529
+ success: false,
530
+ message: "Changelog not found.",
531
+ });
532
+ }
533
+
534
+ // Check ownership through project
535
+ const project = await Project.findById(changelog.projectId);
536
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
537
+ return res.status(403).json({
538
+ success: false,
539
+ message: "Access denied. You do not own this changelog.",
540
+ });
541
+ }
542
+
543
+ // Make changelog private and remove share token
544
+ changelog.isPublic = false;
545
+ changelog.shareToken = null;
546
+ await changelog.save();
547
+
548
+ logger.info(
549
+ `Changelog unshared: ${changelog.title} by user ${req.user.email}`,
550
+ );
551
+
552
+ res.json({
553
+ success: true,
554
+ message: "Changelog unshared successfully.",
555
+ });
556
+ } catch (error) {
557
+ logger.error("Unshare changelog error:", error);
558
+ res.status(500).json({
559
+ success: false,
560
+ message: "Internal server error.",
561
+ });
562
+ }
563
+ };
564
+
565
+ // Get public changelog by share token
566
+ const getPublicChangelog = async (req, res) => {
567
+ try {
568
+ const { shareToken } = req.params;
569
+
570
+ const changelog = await Changelog.findOne({ shareToken, isPublic: true })
571
+ .populate("projectId", "name slug")
572
+ .populate("repositoryId", "name githubRepoName")
573
+ .populate("generatedBy", "name email");
574
+
575
+ if (!changelog) {
576
+ return res.status(404).json({
577
+ success: false,
578
+ message: "Changelog not found or not shared.",
579
+ });
580
+ }
581
+
582
+ // Increment view count
583
+ await changelog.incrementViewCount();
584
+
585
+ res.json({
586
+ success: true,
587
+ data: {
588
+ changelog: changelog.toJSON(),
589
+ },
590
+ });
591
+ } catch (error) {
592
+ logger.error("Get public changelog error:", error);
593
+ res.status(500).json({
594
+ success: false,
595
+ message: "Internal server error.",
596
+ });
597
+ }
598
+ };
599
+
600
+ // Download changelog
601
+ const downloadChangelog = async (req, res) => {
602
+ try {
603
+ const { id } = req.params;
604
+ const { format = "markdown" } = req.query;
605
+
606
+ const changelog = await Changelog.findById(id);
607
+ if (!changelog) {
608
+ return res.status(404).json({
609
+ success: false,
610
+ message: "Changelog not found.",
611
+ });
612
+ }
613
+
614
+ // Check ownership through project
615
+ const project = await Project.findById(changelog.projectId);
616
+ if (!project || project.ownerId.toString() !== req.user._id.toString()) {
617
+ return res.status(403).json({
618
+ success: false,
619
+ message: "Access denied. You do not own this changelog.",
620
+ });
621
+ }
622
+
623
+ // Generate download URL or content
624
+ let downloadUrl = changelog.downloadUrl;
625
+ let content = changelog.content;
626
+
627
+ // If format is different, regenerate content
628
+ if (format !== changelog.format) {
629
+ const repository = await Repository.findById(changelog.repositoryId);
630
+ const month = changelog.month;
631
+ const year = changelog.year;
632
+
633
+ const changelogService = new ChangelogService();
634
+ content = await changelogService.generateContent(
635
+ repository,
636
+ month,
637
+ year,
638
+ {
639
+ format,
640
+ template: changelog.template,
641
+ },
642
+ );
643
+ }
644
+
645
+ // Set appropriate headers for download
646
+ res.setHeader(
647
+ "Content-Type",
648
+ format === "markdown"
649
+ ? "text/markdown"
650
+ : format === "html"
651
+ ? "text/html"
652
+ : "application/json",
653
+ );
654
+ res.setHeader(
655
+ "Content-Disposition",
656
+ `attachment; filename="${changelog.title.replace(/\s+/g, "_")}.${format}"`,
657
+ );
658
+
659
+ res.send(content);
660
+ } catch (error) {
661
+ logger.error("Download changelog error:", error);
662
+ res.status(500).json({
663
+ success: false,
664
+ message: "Internal server error.",
665
+ });
666
+ }
667
+ };
668
+
669
+ module.exports = {
670
+ generateChangelog,
671
+ getChangelogs,
672
+ getRepositoryChangelogs,
673
+ getChangelog,
674
+ updateChangelog,
675
+ deleteChangelog,
676
+ getChangelogStatistics,
677
+ getAvailableMonths,
678
+ shareChangelog,
679
+ unshareChangelog,
680
+ getPublicChangelog,
681
+ downloadChangelog,
682
+ };