@intranefr/superbackend 1.4.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.
Files changed (188) hide show
  1. package/.commiat +4 -0
  2. package/.env.example +47 -0
  3. package/README.md +110 -0
  4. package/index.js +94 -0
  5. package/package.json +67 -0
  6. package/public/css/styles.css +139 -0
  7. package/public/js/animations.js +41 -0
  8. package/sdk/error-tracking/browser/package.json +16 -0
  9. package/sdk/error-tracking/browser/src/core.js +270 -0
  10. package/sdk/error-tracking/browser/src/embed.js +18 -0
  11. package/sdk/error-tracking/browser/src/index.js +1 -0
  12. package/server.js +5 -0
  13. package/src/admin/endpointRegistry.js +300 -0
  14. package/src/controllers/admin.controller.js +321 -0
  15. package/src/controllers/adminAssets.controller.js +530 -0
  16. package/src/controllers/adminAssetsStorage.controller.js +260 -0
  17. package/src/controllers/adminEjsVirtual.controller.js +354 -0
  18. package/src/controllers/adminFeatureFlags.controller.js +155 -0
  19. package/src/controllers/adminHeadless.controller.js +1071 -0
  20. package/src/controllers/adminI18n.controller.js +604 -0
  21. package/src/controllers/adminJsonConfigs.controller.js +97 -0
  22. package/src/controllers/adminLlm.controller.js +273 -0
  23. package/src/controllers/adminMigration.controller.js +257 -0
  24. package/src/controllers/adminSeoConfig.controller.js +515 -0
  25. package/src/controllers/adminStats.controller.js +121 -0
  26. package/src/controllers/adminUploadNamespaces.controller.js +208 -0
  27. package/src/controllers/assets.controller.js +248 -0
  28. package/src/controllers/auth.controller.js +93 -0
  29. package/src/controllers/billing.controller.js +223 -0
  30. package/src/controllers/featureFlags.controller.js +35 -0
  31. package/src/controllers/forms.controller.js +217 -0
  32. package/src/controllers/globalSettings.controller.js +252 -0
  33. package/src/controllers/headlessCrud.controller.js +126 -0
  34. package/src/controllers/i18n.controller.js +12 -0
  35. package/src/controllers/invite.controller.js +249 -0
  36. package/src/controllers/jsonConfigs.controller.js +19 -0
  37. package/src/controllers/metrics.controller.js +149 -0
  38. package/src/controllers/notificationAdmin.controller.js +264 -0
  39. package/src/controllers/notifications.controller.js +131 -0
  40. package/src/controllers/org.controller.js +357 -0
  41. package/src/controllers/orgAdmin.controller.js +491 -0
  42. package/src/controllers/stripeAdmin.controller.js +410 -0
  43. package/src/controllers/user.controller.js +361 -0
  44. package/src/controllers/userAdmin.controller.js +277 -0
  45. package/src/controllers/waitingList.controller.js +167 -0
  46. package/src/controllers/webhook.controller.js +200 -0
  47. package/src/middleware/auth.js +66 -0
  48. package/src/middleware/errorCapture.js +170 -0
  49. package/src/middleware/headlessApiTokenAuth.js +57 -0
  50. package/src/middleware/org.js +108 -0
  51. package/src/middleware.js +901 -0
  52. package/src/models/ActionEvent.js +31 -0
  53. package/src/models/ActivityLog.js +41 -0
  54. package/src/models/Asset.js +84 -0
  55. package/src/models/AuditEvent.js +93 -0
  56. package/src/models/EmailLog.js +28 -0
  57. package/src/models/ErrorAggregate.js +72 -0
  58. package/src/models/FormSubmission.js +41 -0
  59. package/src/models/GlobalSetting.js +38 -0
  60. package/src/models/HeadlessApiToken.js +24 -0
  61. package/src/models/HeadlessModelDefinition.js +41 -0
  62. package/src/models/I18nEntry.js +77 -0
  63. package/src/models/I18nLocale.js +33 -0
  64. package/src/models/Invite.js +70 -0
  65. package/src/models/JsonConfig.js +46 -0
  66. package/src/models/Notification.js +60 -0
  67. package/src/models/Organization.js +57 -0
  68. package/src/models/OrganizationMember.js +43 -0
  69. package/src/models/StripeCatalogItem.js +77 -0
  70. package/src/models/StripeWebhookEvent.js +57 -0
  71. package/src/models/User.js +89 -0
  72. package/src/models/VirtualEjsFile.js +60 -0
  73. package/src/models/VirtualEjsFileVersion.js +43 -0
  74. package/src/models/VirtualEjsGroupChange.js +32 -0
  75. package/src/models/WaitingList.js +41 -0
  76. package/src/models/Webhook.js +63 -0
  77. package/src/models/Workflow.js +29 -0
  78. package/src/models/WorkflowExecution.js +12 -0
  79. package/src/routes/admin.routes.js +26 -0
  80. package/src/routes/adminAssets.routes.js +28 -0
  81. package/src/routes/adminAssetsStorage.routes.js +13 -0
  82. package/src/routes/adminAudit.routes.js +196 -0
  83. package/src/routes/adminEjsVirtual.routes.js +17 -0
  84. package/src/routes/adminErrors.routes.js +164 -0
  85. package/src/routes/adminFeatureFlags.routes.js +12 -0
  86. package/src/routes/adminHeadless.routes.js +38 -0
  87. package/src/routes/adminI18n.routes.js +22 -0
  88. package/src/routes/adminJsonConfigs.routes.js +15 -0
  89. package/src/routes/adminLlm.routes.js +12 -0
  90. package/src/routes/adminMigration.routes.js +81 -0
  91. package/src/routes/adminSeoConfig.routes.js +20 -0
  92. package/src/routes/adminUploadNamespaces.routes.js +13 -0
  93. package/src/routes/assets.routes.js +21 -0
  94. package/src/routes/auth.routes.js +12 -0
  95. package/src/routes/billing.routes.js +11 -0
  96. package/src/routes/errorTracking.routes.js +31 -0
  97. package/src/routes/featureFlags.routes.js +9 -0
  98. package/src/routes/forms.routes.js +9 -0
  99. package/src/routes/formsAdmin.routes.js +13 -0
  100. package/src/routes/globalSettings.routes.js +18 -0
  101. package/src/routes/headless.routes.js +15 -0
  102. package/src/routes/i18n.routes.js +8 -0
  103. package/src/routes/invite.routes.js +9 -0
  104. package/src/routes/jsonConfigs.routes.js +8 -0
  105. package/src/routes/log.routes.js +111 -0
  106. package/src/routes/metrics.routes.js +9 -0
  107. package/src/routes/notificationAdmin.routes.js +15 -0
  108. package/src/routes/notifications.routes.js +12 -0
  109. package/src/routes/org.routes.js +31 -0
  110. package/src/routes/orgAdmin.routes.js +20 -0
  111. package/src/routes/publicAssets.routes.js +7 -0
  112. package/src/routes/stripeAdmin.routes.js +20 -0
  113. package/src/routes/user.routes.js +22 -0
  114. package/src/routes/userAdmin.routes.js +15 -0
  115. package/src/routes/waitingList.routes.js +13 -0
  116. package/src/routes/waitingListAdmin.routes.js +9 -0
  117. package/src/routes/webhook.routes.js +32 -0
  118. package/src/routes/workflowWebhook.routes.js +54 -0
  119. package/src/routes/workflows.routes.js +110 -0
  120. package/src/services/assets.service.js +110 -0
  121. package/src/services/audit.service.js +62 -0
  122. package/src/services/auditLogger.js +165 -0
  123. package/src/services/ejsVirtual.service.js +614 -0
  124. package/src/services/email.service.js +351 -0
  125. package/src/services/errorLogger.js +221 -0
  126. package/src/services/featureFlags.service.js +202 -0
  127. package/src/services/forms.service.js +214 -0
  128. package/src/services/globalSettings.service.js +49 -0
  129. package/src/services/headlessApiTokens.service.js +158 -0
  130. package/src/services/headlessCrypto.service.js +31 -0
  131. package/src/services/headlessModels.service.js +356 -0
  132. package/src/services/i18n.service.js +314 -0
  133. package/src/services/i18nInferredKeys.service.js +337 -0
  134. package/src/services/jsonConfigs.service.js +392 -0
  135. package/src/services/llm.service.js +749 -0
  136. package/src/services/migration.service.js +581 -0
  137. package/src/services/migrationAssets/fsLocal.js +58 -0
  138. package/src/services/migrationAssets/index.js +134 -0
  139. package/src/services/migrationAssets/s3.js +75 -0
  140. package/src/services/migrationAssets/sftp.js +92 -0
  141. package/src/services/notification.service.js +212 -0
  142. package/src/services/objectStorage.service.js +514 -0
  143. package/src/services/seoConfig.service.js +402 -0
  144. package/src/services/storage.js +150 -0
  145. package/src/services/stripe.service.js +185 -0
  146. package/src/services/stripeHelper.service.js +264 -0
  147. package/src/services/uploadNamespaces.service.js +326 -0
  148. package/src/services/webhook.service.js +157 -0
  149. package/src/services/workflow.service.js +271 -0
  150. package/src/utils/asyncHandler.js +5 -0
  151. package/src/utils/encryption.js +80 -0
  152. package/src/utils/jwt.js +40 -0
  153. package/src/utils/orgRoles.js +156 -0
  154. package/src/utils/validation.js +26 -0
  155. package/src/utils/webhookRetry.js +93 -0
  156. package/views/admin-assets.ejs +444 -0
  157. package/views/admin-audit.ejs +283 -0
  158. package/views/admin-coolify-deploy.ejs +207 -0
  159. package/views/admin-dashboard-home.ejs +291 -0
  160. package/views/admin-dashboard.ejs +397 -0
  161. package/views/admin-ejs-virtual.ejs +280 -0
  162. package/views/admin-errors.ejs +368 -0
  163. package/views/admin-feature-flags.ejs +390 -0
  164. package/views/admin-forms.ejs +526 -0
  165. package/views/admin-global-settings.ejs +436 -0
  166. package/views/admin-headless.ejs +2020 -0
  167. package/views/admin-i18n-locales.ejs +221 -0
  168. package/views/admin-i18n.ejs +728 -0
  169. package/views/admin-json-configs.ejs +410 -0
  170. package/views/admin-llm.ejs +884 -0
  171. package/views/admin-metrics.ejs +274 -0
  172. package/views/admin-migration.ejs +814 -0
  173. package/views/admin-notifications.ejs +430 -0
  174. package/views/admin-organizations.ejs +984 -0
  175. package/views/admin-seo-config.ejs +673 -0
  176. package/views/admin-stripe-pricing.ejs +558 -0
  177. package/views/admin-test.ejs +342 -0
  178. package/views/admin-users.ejs +452 -0
  179. package/views/admin-waiting-list.ejs +547 -0
  180. package/views/admin-webhooks.ejs +329 -0
  181. package/views/admin-workflows.ejs +310 -0
  182. package/views/partials/admin-assets-script.ejs +2022 -0
  183. package/views/partials/admin-test-sidebar.ejs +14 -0
  184. package/views/partials/dashboard/nav-items.ejs +66 -0
  185. package/views/partials/dashboard/palette.ejs +63 -0
  186. package/views/partials/dashboard/sidebar.ejs +21 -0
  187. package/views/partials/dashboard/tab-bar.ejs +26 -0
  188. package/views/partials/footer.ejs +3 -0
@@ -0,0 +1,252 @@
1
+ const GlobalSetting = require('../models/GlobalSetting');
2
+ const { encryptString, decryptString } = require('../utils/encryption');
3
+ const globalSettingsService = require('../services/globalSettings.service');
4
+ const { clearOrgRolesCache } = require('../utils/orgRoles');
5
+
6
+ const redactSetting = (setting) => {
7
+ if (!setting) return setting;
8
+ if (setting.type !== 'encrypted') return setting;
9
+ return {
10
+ ...setting,
11
+ value: '********',
12
+ };
13
+ };
14
+
15
+ // GET /api/admin/settings/:key/reveal - Get decrypted value for an encrypted setting
16
+ exports.revealSetting = async (req, res) => {
17
+ try {
18
+ const { key } = req.params;
19
+
20
+ const setting = await GlobalSetting.findOne({ key }).lean();
21
+
22
+ if (!setting) {
23
+ return res.status(404).json({
24
+ error: `Setting with key '${key}' not found.`,
25
+ });
26
+ }
27
+
28
+ if (setting.type === 'encrypted') {
29
+ try {
30
+ const payload = JSON.parse(setting.value);
31
+ const decrypted = decryptString(payload);
32
+ return res.json({
33
+ key: setting.key,
34
+ type: setting.type,
35
+ value: decrypted,
36
+ });
37
+ } catch (e) {
38
+ console.error(`Error decrypting setting ${key}:`, e);
39
+ return res.status(500).json({ error: 'Failed to decrypt setting' });
40
+ }
41
+ }
42
+
43
+ // For non-encrypted settings, just return current value
44
+ return res.json({
45
+ key: setting.key,
46
+ type: setting.type,
47
+ value: setting.value,
48
+ });
49
+ } catch (error) {
50
+ console.error('Error revealing setting:', error);
51
+ res.status(500).json({ error: 'Failed to reveal setting' });
52
+ }
53
+ };
54
+
55
+ // GET /api/admin/settings - Get all global settings
56
+ exports.getAllSettings = async (req, res) => {
57
+ try {
58
+ const settings = await GlobalSetting.find().sort({ key: 1 }).lean();
59
+ res.json(settings.map(redactSetting));
60
+ } catch (error) {
61
+ console.error('Error fetching global settings:', error);
62
+ res.status(500).json({ error: 'Failed to fetch global settings' });
63
+ }
64
+ };
65
+
66
+ // GET /api/admin/settings/:key - Get specific setting
67
+ exports.getSetting = async (req, res) => {
68
+ try {
69
+ const { key } = req.params;
70
+
71
+ const setting = await GlobalSetting.findOne({ key }).lean();
72
+
73
+ if (!setting) {
74
+ return res.status(404).json({
75
+ error: `Setting with key '${key}' not found.`
76
+ });
77
+ }
78
+
79
+ res.json(redactSetting(setting));
80
+ } catch (error) {
81
+ console.error('Error fetching setting:', error);
82
+ res.status(500).json({ error: 'Failed to fetch setting' });
83
+ }
84
+ };
85
+
86
+ // PUT /api/admin/settings/:key - Update setting
87
+ exports.updateSetting = async (req, res) => {
88
+ try {
89
+ const { key } = req.params;
90
+ const { value } = req.body;
91
+
92
+ if (value === undefined) {
93
+ return res.status(400).json({ error: 'Value is required' });
94
+ }
95
+
96
+ const setting = await GlobalSetting.findOne({ key });
97
+
98
+ if (!setting) {
99
+ return res.status(404).json({
100
+ error: `Setting with key '${key}' not found.`
101
+ });
102
+ }
103
+
104
+ // Validate value based on type
105
+ if (setting.type === 'boolean') {
106
+ if (value !== 'true' && value !== 'false') {
107
+ return res.status(400).json({
108
+ error: 'Boolean setting must be "true" or "false"'
109
+ });
110
+ }
111
+ } else if (setting.type === 'number') {
112
+ if (isNaN(Number(value))) {
113
+ return res.status(400).json({
114
+ error: 'Number setting must be a valid number'
115
+ });
116
+ }
117
+ } else if (setting.type === 'json') {
118
+ try {
119
+ JSON.parse(value);
120
+ } catch (e) {
121
+ return res.status(400).json({
122
+ error: 'JSON setting must be valid JSON'
123
+ });
124
+ }
125
+ }
126
+
127
+ if (setting.type === 'encrypted') {
128
+ const encryptedPayload = encryptString(value);
129
+ setting.value = JSON.stringify(encryptedPayload);
130
+ } else {
131
+ setting.value = value;
132
+ }
133
+ await setting.save();
134
+
135
+ globalSettingsService.clearSettingsCache();
136
+ if (key === 'ORG_ROLES_JSON') {
137
+ clearOrgRolesCache();
138
+ }
139
+
140
+ res.json(redactSetting(setting.toObject()));
141
+ } catch (error) {
142
+ console.error('Error updating setting:', error);
143
+ res.status(500).json({ error: 'Failed to update setting' });
144
+ }
145
+ };
146
+
147
+ // POST /api/admin/settings - Create new setting
148
+ exports.createSetting = async (req, res) => {
149
+ try {
150
+ const { key, value, type, description, templateVariables, public: isPublic } = req.body;
151
+
152
+ if (!key || !value || !type || !description) {
153
+ return res.status(400).json({
154
+ error: 'key, value, type, and description are required'
155
+ });
156
+ }
157
+
158
+ // Check if setting already exists
159
+ const existingSetting = await GlobalSetting.findOne({ key });
160
+ if (existingSetting) {
161
+ return res.status(409).json({
162
+ error: `Setting with key '${key}' already exists.`
163
+ });
164
+ }
165
+
166
+ if (type === 'encrypted' && isPublic) {
167
+ return res.status(400).json({ error: 'Encrypted settings cannot be public' });
168
+ }
169
+
170
+ const storedValue =
171
+ type === 'encrypted' ? JSON.stringify(encryptString(value)) : value;
172
+
173
+ const setting = await GlobalSetting.create({
174
+ key,
175
+ value: storedValue,
176
+ type,
177
+ description,
178
+ templateVariables: templateVariables || [],
179
+ public: isPublic || false
180
+ });
181
+
182
+ globalSettingsService.clearSettingsCache();
183
+ if (key === 'ORG_ROLES_JSON') {
184
+ clearOrgRolesCache();
185
+ }
186
+
187
+ res.status(201).json(redactSetting(setting.toObject()));
188
+ } catch (error) {
189
+ console.error('Error creating setting:', error);
190
+ res.status(500).json({ error: 'Failed to create setting' });
191
+ }
192
+ };
193
+
194
+ // DELETE /api/admin/settings/:key - Delete setting
195
+ exports.deleteSetting = async (req, res) => {
196
+ try {
197
+ const { key } = req.params;
198
+
199
+ const setting = await GlobalSetting.findOneAndDelete({ key });
200
+
201
+ if (!setting) {
202
+ return res.status(404).json({
203
+ error: `Setting with key '${key}' not found.`
204
+ });
205
+ }
206
+
207
+ globalSettingsService.clearSettingsCache();
208
+ if (key === 'ORG_ROLES_JSON') {
209
+ clearOrgRolesCache();
210
+ }
211
+
212
+ res.status(204).send();
213
+ } catch (error) {
214
+ console.error('Error deleting setting:', error);
215
+ res.status(500).json({ error: 'Failed to delete setting' });
216
+ }
217
+ };
218
+
219
+ // Helper function to get setting value (for internal use)
220
+ exports.getSettingValue = async (key, defaultValue = null) => {
221
+ try {
222
+ const setting = await GlobalSetting.findOne({ key }).lean();
223
+ if (!setting) return defaultValue;
224
+ if (setting.type !== 'encrypted') return setting.value;
225
+
226
+ try {
227
+ const payload = JSON.parse(setting.value);
228
+ return decryptString(payload);
229
+ } catch (e) {
230
+ console.error(`Error decrypting setting ${key}:`, e);
231
+ return defaultValue;
232
+ }
233
+ } catch (error) {
234
+ console.error(`Error getting setting ${key}:`, error);
235
+ return defaultValue;
236
+ }
237
+ };
238
+
239
+ // GET /api/settings/public - Get public settings (no auth required)
240
+ exports.getPublicSettings = async (req, res) => {
241
+ try {
242
+ const settings = await GlobalSetting.find({ public: true })
243
+ .select('key value type description')
244
+ .sort({ key: 1 })
245
+ .lean();
246
+
247
+ res.json(settings);
248
+ } catch (error) {
249
+ console.error('Error fetching public settings:', error);
250
+ res.status(500).json({ error: 'Failed to fetch public settings' });
251
+ }
252
+ };
@@ -0,0 +1,126 @@
1
+ const { getDynamicModel } = require('../services/headlessModels.service');
2
+
3
+ function getOperationFromRequest(req) {
4
+ const method = String(req.method || '').toUpperCase();
5
+ if (method === 'GET') return 'read';
6
+ if (method === 'POST') return 'create';
7
+ if (method === 'PUT' || method === 'PATCH') return 'update';
8
+ if (method === 'DELETE') return 'delete';
9
+ return null;
10
+ }
11
+
12
+ function parseJsonMaybe(value) {
13
+ if (value === undefined || value === null) return null;
14
+ if (typeof value !== 'string') return value;
15
+ const trimmed = value.trim();
16
+ if (!trimmed) return null;
17
+ try {
18
+ return JSON.parse(trimmed);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ async function maybePopulate(Model, query, populateParam) {
25
+ const populate = String(populateParam || '').trim();
26
+ if (!populate) return query;
27
+
28
+ const fields = populate
29
+ .split(',')
30
+ .map((f) => f.trim())
31
+ .filter((f) => f);
32
+
33
+ for (const field of fields) {
34
+ query = query.populate(field);
35
+ }
36
+
37
+ return query;
38
+ }
39
+
40
+ exports.list = async (req, res) => {
41
+ try {
42
+ const { modelCode } = req.params;
43
+ const Model = await getDynamicModel(modelCode);
44
+
45
+ const limit = Math.min(Number(req.query.limit || 50) || 50, 200);
46
+ const skip = Number(req.query.skip || 0) || 0;
47
+ const sort = parseJsonMaybe(req.query.sort) || { updatedAt: -1 };
48
+ const filter = parseJsonMaybe(req.query.filter) || {};
49
+
50
+ let q = Model.find(filter).sort(sort).skip(skip).limit(limit);
51
+ q = await maybePopulate(Model, q, req.query.populate);
52
+
53
+ const items = await q.lean();
54
+ const total = await Model.countDocuments(filter);
55
+
56
+ return res.json({ items, total, limit, skip });
57
+ } catch (error) {
58
+ console.error('Error listing headless items:', error);
59
+ return res.status(500).json({ error: 'Failed to list items' });
60
+ }
61
+ };
62
+
63
+ exports.get = async (req, res) => {
64
+ try {
65
+ const { modelCode, id } = req.params;
66
+ const Model = await getDynamicModel(modelCode);
67
+
68
+ let q = Model.findById(id);
69
+ q = await maybePopulate(Model, q, req.query.populate);
70
+ const item = await q.lean();
71
+
72
+ if (!item) return res.status(404).json({ error: 'Item not found' });
73
+ return res.json({ item });
74
+ } catch (error) {
75
+ console.error('Error fetching headless item:', error);
76
+ return res.status(500).json({ error: 'Failed to fetch item' });
77
+ }
78
+ };
79
+
80
+ exports.create = async (req, res) => {
81
+ try {
82
+ const { modelCode } = req.params;
83
+ const Model = await getDynamicModel(modelCode);
84
+
85
+ const doc = await Model.create(req.body || {});
86
+ return res.status(201).json({ item: doc.toObject() });
87
+ } catch (error) {
88
+ console.error('Error creating headless item:', error);
89
+ return res.status(500).json({ error: 'Failed to create item' });
90
+ }
91
+ };
92
+
93
+ exports.update = async (req, res) => {
94
+ try {
95
+ const { modelCode, id } = req.params;
96
+ const Model = await getDynamicModel(modelCode);
97
+
98
+ const updated = await Model.findByIdAndUpdate(id, req.body || {}, {
99
+ new: true,
100
+ runValidators: false,
101
+ });
102
+
103
+ if (!updated) return res.status(404).json({ error: 'Item not found' });
104
+ return res.json({ item: updated.toObject() });
105
+ } catch (error) {
106
+ console.error('Error updating headless item:', error);
107
+ return res.status(500).json({ error: 'Failed to update item' });
108
+ }
109
+ };
110
+
111
+ exports.remove = async (req, res) => {
112
+ try {
113
+ const { modelCode, id } = req.params;
114
+ const Model = await getDynamicModel(modelCode);
115
+
116
+ const deleted = await Model.findByIdAndDelete(id);
117
+ if (!deleted) return res.status(404).json({ error: 'Item not found' });
118
+
119
+ return res.json({ success: true });
120
+ } catch (error) {
121
+ console.error('Error deleting headless item:', error);
122
+ return res.status(500).json({ error: 'Failed to delete item' });
123
+ }
124
+ };
125
+
126
+ exports._getOperationFromRequest = getOperationFromRequest;
@@ -0,0 +1,12 @@
1
+ const i18nService = require('../services/i18n.service');
2
+
3
+ exports.getBundle = async (req, res) => {
4
+ try {
5
+ const locale = req.query.locale;
6
+ const bundle = await i18nService.getBundle(locale);
7
+ res.json(bundle);
8
+ } catch (error) {
9
+ console.error('Error building i18n bundle:', error);
10
+ res.status(500).json({ error: 'Failed to build i18n bundle' });
11
+ }
12
+ };
@@ -0,0 +1,249 @@
1
+ const Invite = require('../models/Invite');
2
+ const Organization = require('../models/Organization');
3
+ const OrganizationMember = require('../models/OrganizationMember');
4
+ const User = require('../models/User');
5
+ const emailService = require('../services/email.service');
6
+ const bcrypt = require('bcryptjs');
7
+ const { isValidOrgRole, getAllowedOrgRoles, getDefaultOrgRole } = require('../utils/orgRoles');
8
+
9
+ const INVITE_EXPIRY_DAYS = 7;
10
+
11
+ exports.createInvite = async (req, res) => {
12
+ try {
13
+ const defaultRole = await getDefaultOrgRole();
14
+ const { email, role = defaultRole } = req.body;
15
+
16
+ if (!email) {
17
+ return res.status(400).json({ error: 'Email is required' });
18
+ }
19
+
20
+ if (!(await isValidOrgRole(role))) {
21
+ const allowed = await getAllowedOrgRoles();
22
+ return res.status(400).json({ error: 'Invalid role', allowedRoles: allowed });
23
+ }
24
+
25
+ const normalizedEmail = email.toLowerCase().trim();
26
+
27
+ const existingUser = await User.findOne({ email: normalizedEmail });
28
+ if (existingUser) {
29
+ const existingMember = await OrganizationMember.findOne({
30
+ orgId: req.org._id,
31
+ userId: existingUser._id,
32
+ status: 'active'
33
+ });
34
+ if (existingMember) {
35
+ return res.status(409).json({ error: 'User is already a member' });
36
+ }
37
+ }
38
+
39
+ const existingInvite = await Invite.findOne({
40
+ email: normalizedEmail,
41
+ orgId: req.org._id,
42
+ status: 'pending'
43
+ });
44
+ if (existingInvite) {
45
+ return res.status(409).json({ error: 'Invite already pending for this email' });
46
+ }
47
+
48
+ const { token, tokenHash } = Invite.generateToken();
49
+ const expiresAt = new Date(Date.now() + INVITE_EXPIRY_DAYS * 24 * 60 * 60 * 1000);
50
+
51
+ const invite = await Invite.create({
52
+ email: normalizedEmail,
53
+ tokenHash,
54
+ expiresAt,
55
+ createdByUserId: req.user._id,
56
+ orgId: req.org._id,
57
+ role
58
+ });
59
+
60
+ const inviteLink = `${process.env.PUBLIC_URL || 'http://localhost:3000'}/accept-invite?token=${token}`;
61
+
62
+ try {
63
+ await emailService.sendEmail({
64
+ to: normalizedEmail,
65
+ subject: `You're invited to join ${req.org.name}`,
66
+ html: `<p>You've been invited to join <strong>${req.org.name}</strong> as a ${role}.</p>
67
+ <p><a href="${inviteLink}">Click here to accept the invitation</a></p>
68
+ <p>This invite expires in ${INVITE_EXPIRY_DAYS} days.</p>
69
+ <p>If you didn't expect this invitation, you can ignore this email.</p>`
70
+ });
71
+ } catch (emailError) {
72
+ console.error('Failed to send invite email:', emailError);
73
+ }
74
+
75
+ res.status(201).json({
76
+ message: 'Invite created successfully',
77
+ invite: {
78
+ _id: invite._id,
79
+ email: invite.email,
80
+ role: invite.role,
81
+ expiresAt: invite.expiresAt,
82
+ createdAt: invite.createdAt
83
+ }
84
+ });
85
+ } catch (error) {
86
+ console.error('Error creating invite:', error);
87
+ res.status(500).json({ error: 'Failed to create invite' });
88
+ }
89
+ };
90
+
91
+ exports.listInvites = async (req, res) => {
92
+ try {
93
+ const invites = await Invite.find({
94
+ orgId: req.org._id,
95
+ status: 'pending'
96
+ }).select('-tokenHash');
97
+
98
+ res.json({ invites });
99
+ } catch (error) {
100
+ console.error('Error listing invites:', error);
101
+ res.status(500).json({ error: 'Failed to list invites' });
102
+ }
103
+ };
104
+
105
+ exports.revokeInvite = async (req, res) => {
106
+ try {
107
+ const { inviteId } = req.params;
108
+
109
+ const invite = await Invite.findOne({
110
+ _id: inviteId,
111
+ orgId: req.org._id,
112
+ status: 'pending'
113
+ });
114
+
115
+ if (!invite) {
116
+ return res.status(404).json({ error: 'Invite not found' });
117
+ }
118
+
119
+ invite.status = 'revoked';
120
+ await invite.save();
121
+
122
+ res.json({ message: 'Invite revoked successfully' });
123
+ } catch (error) {
124
+ console.error('Error revoking invite:', error);
125
+ res.status(500).json({ error: 'Failed to revoke invite' });
126
+ }
127
+ };
128
+
129
+ exports.acceptInvite = async (req, res) => {
130
+ try {
131
+ const { token, name, password } = req.body;
132
+
133
+ if (!token) {
134
+ return res.status(400).json({ error: 'Token is required' });
135
+ }
136
+
137
+ const tokenHash = Invite.hashToken(token);
138
+ const invite = await Invite.findOne({ tokenHash }).populate('orgId');
139
+
140
+ if (!invite) {
141
+ return res.status(404).json({ error: 'Invalid invite token' });
142
+ }
143
+
144
+ if (invite.status !== 'pending') {
145
+ return res.status(400).json({ error: `Invite has been ${invite.status}` });
146
+ }
147
+
148
+ if (invite.expiresAt < new Date()) {
149
+ invite.status = 'expired';
150
+ await invite.save();
151
+ return res.status(400).json({ error: 'Invite has expired' });
152
+ }
153
+
154
+ let user = await User.findOne({ email: invite.email });
155
+
156
+ if (!user) {
157
+ if (!password || password.length < 6) {
158
+ return res.status(400).json({ error: 'Password required (min 6 characters) for new account' });
159
+ }
160
+
161
+ user = await User.create({
162
+ email: invite.email,
163
+ passwordHash: password,
164
+ name: name?.trim()
165
+ });
166
+ }
167
+
168
+ const existingMember = await OrganizationMember.findOne({
169
+ orgId: invite.orgId._id,
170
+ userId: user._id
171
+ });
172
+
173
+ if (existingMember) {
174
+ if (existingMember.status === 'active') {
175
+ invite.status = 'accepted';
176
+ await invite.save();
177
+ return res.status(409).json({ error: 'Already a member of this organization' });
178
+ }
179
+ existingMember.status = 'active';
180
+ existingMember.role = invite.role;
181
+ await existingMember.save();
182
+ } else {
183
+ await OrganizationMember.create({
184
+ orgId: invite.orgId._id,
185
+ userId: user._id,
186
+ role: invite.role
187
+ });
188
+ }
189
+
190
+ invite.status = 'accepted';
191
+ await invite.save();
192
+
193
+ res.json({
194
+ message: 'Invite accepted successfully',
195
+ org: {
196
+ _id: invite.orgId._id,
197
+ name: invite.orgId.name,
198
+ slug: invite.orgId.slug
199
+ },
200
+ isNewUser: !user.createdAt || (new Date() - user.createdAt) < 5000
201
+ });
202
+ } catch (error) {
203
+ console.error('Error accepting invite:', error);
204
+ res.status(500).json({ error: 'Failed to accept invite' });
205
+ }
206
+ };
207
+
208
+ exports.getInviteInfo = async (req, res) => {
209
+ try {
210
+ const { token } = req.query;
211
+
212
+ if (!token) {
213
+ return res.status(400).json({ error: 'Token is required' });
214
+ }
215
+
216
+ const tokenHash = Invite.hashToken(token);
217
+ const invite = await Invite.findOne({ tokenHash }).populate('orgId', 'name slug');
218
+
219
+ if (!invite) {
220
+ return res.status(404).json({ error: 'Invalid invite token' });
221
+ }
222
+
223
+ if (invite.status !== 'pending') {
224
+ return res.status(400).json({ error: `Invite has been ${invite.status}` });
225
+ }
226
+
227
+ if (invite.expiresAt < new Date()) {
228
+ return res.status(400).json({ error: 'Invite has expired' });
229
+ }
230
+
231
+ const existingUser = await User.findOne({ email: invite.email });
232
+
233
+ res.json({
234
+ invite: {
235
+ email: invite.email,
236
+ role: invite.role,
237
+ expiresAt: invite.expiresAt,
238
+ org: {
239
+ name: invite.orgId.name,
240
+ slug: invite.orgId.slug
241
+ }
242
+ },
243
+ userExists: !!existingUser
244
+ });
245
+ } catch (error) {
246
+ console.error('Error getting invite info:', error);
247
+ res.status(500).json({ error: 'Failed to get invite info' });
248
+ }
249
+ };
@@ -0,0 +1,19 @@
1
+ const { getJsonConfigPublicPayload } = require('../services/jsonConfigs.service');
2
+
3
+ exports.getPublic = async (req, res) => {
4
+ try {
5
+ const slug = String(req.params.slug || '').trim();
6
+ const raw = req.query?.raw === 'true' || req.query?.raw === '1';
7
+
8
+ const payload = await getJsonConfigPublicPayload(slug, { raw });
9
+ return res.json(payload);
10
+ } catch (error) {
11
+ const code = error?.code;
12
+ if (code === 'NOT_FOUND') {
13
+ return res.status(404).json({ error: 'Not found' });
14
+ }
15
+
16
+ console.error('Error fetching public JSON config:', error);
17
+ return res.status(500).json({ error: error?.message || 'Failed to fetch config' });
18
+ }
19
+ };