@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,208 @@
1
+ const GlobalSetting = require('../models/GlobalSetting');
2
+ const Asset = require('../models/Asset');
3
+ const uploadNamespacesService = require('../services/uploadNamespaces.service');
4
+ const globalSettingsService = require('../services/globalSettings.service');
5
+
6
+ const parseJson = (value) => {
7
+ if (!value) return {};
8
+ if (typeof value === 'object') return value;
9
+ try {
10
+ return JSON.parse(value);
11
+ } catch {
12
+ return {};
13
+ }
14
+ };
15
+
16
+ exports.listNamespaces = async (req, res) => {
17
+ try {
18
+ const namespaces = await uploadNamespacesService.listNamespaces();
19
+ res.json(namespaces);
20
+ } catch (error) {
21
+ console.error('Error listing upload namespaces:', error);
22
+ res.status(500).json({ error: 'Failed to list upload namespaces' });
23
+ }
24
+ };
25
+
26
+ exports.getNamespacesSummary = async (req, res) => {
27
+ try {
28
+ let namespaces = await uploadNamespacesService.listNamespaces();
29
+
30
+ if (!namespaces.find((n) => n.key === 'default')) {
31
+ namespaces = [await uploadNamespacesService.resolveNamespace('default'), ...namespaces];
32
+ }
33
+
34
+ const stats = await Asset.aggregate([
35
+ { $match: { status: 'uploaded' } },
36
+ {
37
+ $group: {
38
+ _id: { $ifNull: ['$namespace', 'default'] },
39
+ totalFiles: { $sum: 1 },
40
+ totalBytes: { $sum: '$sizeBytes' },
41
+ },
42
+ },
43
+ ]);
44
+
45
+ const statsByNamespace = new Map(stats.map((s) => [String(s._id), s]));
46
+
47
+ const summary = namespaces
48
+ .slice()
49
+ .sort((a, b) => String(a.key).localeCompare(String(b.key)))
50
+ .map((ns) => {
51
+ const s = statsByNamespace.get(String(ns.key)) || { totalFiles: 0, totalBytes: 0 };
52
+ return {
53
+ key: ns.key,
54
+ enabled: ns.enabled !== false,
55
+ maxFileSizeBytes: ns.maxFileSizeBytes,
56
+ allowedContentTypes: ns.allowedContentTypes,
57
+ keyPrefix: ns.keyPrefix,
58
+ defaultVisibility: ns.defaultVisibility,
59
+ enforceVisibility: Boolean(ns.enforceVisibility),
60
+ stats: {
61
+ totalFiles: s.totalFiles || 0,
62
+ totalBytes: s.totalBytes || 0,
63
+ },
64
+ };
65
+ });
66
+
67
+ res.json(summary);
68
+ } catch (error) {
69
+ console.error('Error getting upload namespaces summary:', error);
70
+ res.status(500).json({ error: 'Failed to get upload namespaces summary' });
71
+ }
72
+ };
73
+
74
+ exports.getNamespace = async (req, res) => {
75
+ try {
76
+ const key = String(req.params.key || '').trim();
77
+ if (!key) {
78
+ return res.status(400).json({ error: 'key is required' });
79
+ }
80
+
81
+ const setting = await GlobalSetting.findOne({
82
+ key: uploadNamespacesService.getSettingKey(key),
83
+ type: 'json',
84
+ }).lean();
85
+
86
+ if (!setting) {
87
+ const resolved = await uploadNamespacesService.resolveNamespace(key);
88
+ return res.json(resolved);
89
+ }
90
+
91
+ const raw = parseJson(setting.value);
92
+ const normalized = uploadNamespacesService.normalizePayload(key, raw);
93
+ const merged = await uploadNamespacesService.resolveNamespace(key);
94
+
95
+ res.json({
96
+ ...merged,
97
+ ...normalized,
98
+ createdAt: setting.createdAt,
99
+ updatedAt: setting.updatedAt,
100
+ });
101
+ } catch (error) {
102
+ console.error('Error getting upload namespace:', error);
103
+ res.status(500).json({ error: 'Failed to get upload namespace' });
104
+ }
105
+ };
106
+
107
+ exports.createNamespace = async (req, res) => {
108
+ try {
109
+ const key = req.body?.key ? String(req.body.key).trim() : '';
110
+ if (!key) {
111
+ return res.status(400).json({ error: 'key is required' });
112
+ }
113
+
114
+ const existing = await GlobalSetting.findOne({ key: uploadNamespacesService.getSettingKey(key) }).lean();
115
+ if (existing) {
116
+ return res.status(409).json({ error: `Upload namespace '${key}' already exists` });
117
+ }
118
+
119
+ const normalized = uploadNamespacesService.normalizePayload(key, req.body || {});
120
+
121
+ const setting = await GlobalSetting.create({
122
+ key: uploadNamespacesService.getSettingKey(key),
123
+ type: 'json',
124
+ public: false,
125
+ description: `Upload namespace: ${key}`,
126
+ value: JSON.stringify(normalized),
127
+ });
128
+
129
+ globalSettingsService.clearSettingsCache();
130
+
131
+ const resolved = await uploadNamespacesService.resolveNamespace(key);
132
+ res.status(201).json({
133
+ ...resolved,
134
+ createdAt: setting.createdAt,
135
+ updatedAt: setting.updatedAt,
136
+ });
137
+ } catch (error) {
138
+ console.error('Error creating upload namespace:', error);
139
+ res.status(500).json({ error: 'Failed to create upload namespace' });
140
+ }
141
+ };
142
+
143
+ exports.updateNamespace = async (req, res) => {
144
+ try {
145
+ const key = String(req.params.key || '').trim();
146
+ if (!key) {
147
+ return res.status(400).json({ error: 'key is required' });
148
+ }
149
+
150
+ const settingKey = uploadNamespacesService.getSettingKey(key);
151
+ const existing = await GlobalSetting.findOne({ key: settingKey, type: 'json' });
152
+
153
+ const current = existing ? parseJson(existing.value) : {};
154
+ const mergedRaw = { ...current, ...req.body };
155
+ const normalized = uploadNamespacesService.normalizePayload(key, mergedRaw);
156
+
157
+ const setting = existing
158
+ ? await GlobalSetting.findOneAndUpdate(
159
+ { key: settingKey, type: 'json' },
160
+ { $set: { value: JSON.stringify(normalized) } },
161
+ { new: true }
162
+ )
163
+ : await GlobalSetting.create({
164
+ key: settingKey,
165
+ type: 'json',
166
+ public: false,
167
+ description: `Upload namespace: ${key}`,
168
+ value: JSON.stringify(normalized),
169
+ });
170
+
171
+ globalSettingsService.clearSettingsCache();
172
+
173
+ const resolved = await uploadNamespacesService.resolveNamespace(key);
174
+ res.json({
175
+ ...resolved,
176
+ createdAt: setting.createdAt,
177
+ updatedAt: setting.updatedAt,
178
+ });
179
+ } catch (error) {
180
+ console.error('Error updating upload namespace:', error);
181
+ res.status(500).json({ error: 'Failed to update upload namespace' });
182
+ }
183
+ };
184
+
185
+ exports.deleteNamespace = async (req, res) => {
186
+ try {
187
+ const key = String(req.params.key || '').trim();
188
+ if (!key) {
189
+ return res.status(400).json({ error: 'key is required' });
190
+ }
191
+
192
+ const setting = await GlobalSetting.findOneAndDelete({
193
+ key: uploadNamespacesService.getSettingKey(key),
194
+ type: 'json',
195
+ }).lean();
196
+
197
+ if (!setting) {
198
+ return res.status(404).json({ error: `Upload namespace '${key}' not found` });
199
+ }
200
+
201
+ globalSettingsService.clearSettingsCache();
202
+
203
+ res.json({ success: true });
204
+ } catch (error) {
205
+ console.error('Error deleting upload namespace:', error);
206
+ res.status(500).json({ error: 'Failed to delete upload namespace' });
207
+ }
208
+ };
@@ -0,0 +1,248 @@
1
+ const Asset = require('../models/Asset');
2
+ const objectStorage = require('../services/objectStorage.service');
3
+ const uploadNamespacesService = require('../services/uploadNamespaces.service');
4
+
5
+ const buildPublicUrl = (key) => {
6
+ return `/public/assets/${key}`;
7
+ };
8
+
9
+ const formatAssetResponse = (asset) => {
10
+ const obj = asset.toObject ? asset.toObject() : asset;
11
+ const response = {
12
+ _id: obj._id,
13
+ key: obj.key,
14
+ originalName: obj.originalName,
15
+ contentType: obj.contentType,
16
+ sizeBytes: obj.sizeBytes,
17
+ visibility: obj.visibility,
18
+ status: obj.status,
19
+ createdAt: obj.createdAt,
20
+ updatedAt: obj.updatedAt
21
+ };
22
+
23
+ if (obj.visibility === 'public') {
24
+ response.publicUrl = buildPublicUrl(obj.key);
25
+ }
26
+
27
+ return response;
28
+ };
29
+
30
+ exports.upload = async (req, res) => {
31
+ try {
32
+ if (!req.file && !req.files?.file) {
33
+ return res.status(400).json({ error: 'No file provided' });
34
+ }
35
+
36
+ const file = req.file || req.files.file;
37
+ const buffer = file.buffer || (file.data ? file.data : null);
38
+
39
+ if (!buffer) {
40
+ return res.status(400).json({ error: 'Unable to read file buffer' });
41
+ }
42
+
43
+ const contentType = file.mimetype;
44
+ const originalName = file.originalname || file.name;
45
+ const sizeBytes = buffer.length;
46
+
47
+ const namespaceKey = req.body?.namespace ? String(req.body.namespace).trim() : 'default';
48
+ const namespaceConfig = await uploadNamespacesService.resolveNamespace(namespaceKey);
49
+
50
+ const hardCapMaxFileSizeBytes = await uploadNamespacesService.getEffectiveHardCapMaxFileSizeBytes();
51
+
52
+ const validation = uploadNamespacesService.validateUpload({
53
+ namespaceConfig,
54
+ contentType,
55
+ sizeBytes,
56
+ hardCapMaxFileSizeBytes,
57
+ });
58
+
59
+ if (!validation.ok) {
60
+ return res.status(400).json({
61
+ error: 'Upload rejected by namespace policy',
62
+ namespace: namespaceConfig.key,
63
+ hardCapMaxFileSizeBytes,
64
+ errors: validation.errors,
65
+ });
66
+ }
67
+
68
+ const key = uploadNamespacesService.generateObjectKey({
69
+ namespaceConfig,
70
+ originalName,
71
+ });
72
+
73
+ const visibility = uploadNamespacesService.computeVisibility({
74
+ namespaceConfig,
75
+ requestedVisibility: req.body?.visibility,
76
+ });
77
+
78
+ const orgId = req.body.orgId || null;
79
+
80
+ const { provider, bucket } = await objectStorage.putObject({
81
+ key,
82
+ body: buffer,
83
+ contentType
84
+ });
85
+
86
+ const asset = await Asset.create({
87
+ key,
88
+ provider,
89
+ bucket,
90
+ originalName,
91
+ contentType,
92
+ sizeBytes,
93
+ visibility,
94
+ namespace: namespaceConfig.key,
95
+ visibilityEnforced: Boolean(namespaceConfig.enforceVisibility),
96
+ ownerUserId: req.user._id,
97
+ orgId,
98
+ status: 'uploaded'
99
+ });
100
+
101
+ res.status(201).json({ asset: formatAssetResponse(asset) });
102
+ } catch (error) {
103
+ console.error('Error uploading asset:', error);
104
+ res.status(500).json({ error: 'Failed to upload asset' });
105
+ }
106
+ };
107
+
108
+ exports.list = async (req, res) => {
109
+ try {
110
+ const page = Math.max(1, parseInt(req.query.page) || 1);
111
+ const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
112
+ const skip = (page - 1) * limit;
113
+
114
+ const filter = {
115
+ ownerUserId: req.user._id,
116
+ status: 'uploaded'
117
+ };
118
+
119
+ if (req.query.visibility) {
120
+ filter.visibility = req.query.visibility;
121
+ }
122
+
123
+ if (req.query.orgId) {
124
+ filter.orgId = req.query.orgId;
125
+ }
126
+
127
+ const [assets, total] = await Promise.all([
128
+ Asset.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
129
+ Asset.countDocuments(filter)
130
+ ]);
131
+
132
+ res.json({
133
+ assets: assets.map(formatAssetResponse),
134
+ pagination: {
135
+ page,
136
+ limit,
137
+ total,
138
+ pages: Math.ceil(total / limit)
139
+ }
140
+ });
141
+ } catch (error) {
142
+ console.error('Error listing assets:', error);
143
+ res.status(500).json({ error: 'Failed to list assets' });
144
+ }
145
+ };
146
+
147
+ exports.get = async (req, res) => {
148
+ try {
149
+ const asset = await Asset.findOne({
150
+ _id: req.params.id,
151
+ ownerUserId: req.user._id,
152
+ status: 'uploaded'
153
+ }).lean();
154
+
155
+ if (!asset) {
156
+ return res.status(404).json({ error: 'Asset not found' });
157
+ }
158
+
159
+ res.json({ asset: formatAssetResponse(asset) });
160
+ } catch (error) {
161
+ console.error('Error getting asset:', error);
162
+ res.status(500).json({ error: 'Failed to get asset' });
163
+ }
164
+ };
165
+
166
+ exports.download = async (req, res) => {
167
+ try {
168
+ const asset = await Asset.findOne({
169
+ _id: req.params.id,
170
+ ownerUserId: req.user._id,
171
+ status: 'uploaded'
172
+ }).lean();
173
+
174
+ if (!asset) {
175
+ return res.status(404).json({ error: 'Asset not found' });
176
+ }
177
+
178
+ const result = await objectStorage.getObject({ key: asset.key });
179
+
180
+ if (!result) {
181
+ return res.status(404).json({ error: 'File not found in storage' });
182
+ }
183
+
184
+ res.set('Content-Type', asset.contentType);
185
+ res.set('Content-Disposition', `attachment; filename="${asset.originalName}"`);
186
+ res.send(result.body);
187
+ } catch (error) {
188
+ console.error('Error downloading asset:', error);
189
+ res.status(500).json({ error: 'Failed to download asset' });
190
+ }
191
+ };
192
+
193
+ exports.delete = async (req, res) => {
194
+ try {
195
+ const asset = await Asset.findOne({
196
+ _id: req.params.id,
197
+ ownerUserId: req.user._id,
198
+ status: 'uploaded'
199
+ });
200
+
201
+ if (!asset) {
202
+ return res.status(404).json({ error: 'Asset not found' });
203
+ }
204
+
205
+ await objectStorage.deleteObject({ key: asset.key });
206
+
207
+ asset.status = 'deleted';
208
+ await asset.save();
209
+
210
+ res.json({ success: true });
211
+ } catch (error) {
212
+ console.error('Error deleting asset:', error);
213
+ res.status(500).json({ error: 'Failed to delete asset' });
214
+ }
215
+ };
216
+
217
+ exports.getPublicAsset = async (req, res) => {
218
+ try {
219
+ const key = req.params[0] || req.params.key;
220
+
221
+ if (!key) {
222
+ return res.status(400).json({ error: 'Asset key required' });
223
+ }
224
+
225
+ const asset = await Asset.findOne({
226
+ key,
227
+ visibility: 'public',
228
+ status: 'uploaded'
229
+ }).lean();
230
+
231
+ if (!asset) {
232
+ return res.status(404).json({ error: 'Asset not found' });
233
+ }
234
+
235
+ const result = await objectStorage.getObject({ key: asset.key });
236
+
237
+ if (!result) {
238
+ return res.status(404).json({ error: 'File not found in storage' });
239
+ }
240
+
241
+ res.set('Content-Type', asset.contentType);
242
+ res.set('Cache-Control', 'public, max-age=31536000');
243
+ res.send(result.body);
244
+ } catch (error) {
245
+ console.error('Error serving public asset:', error);
246
+ res.status(500).json({ error: 'Failed to serve asset' });
247
+ }
248
+ };
@@ -0,0 +1,93 @@
1
+ const User = require('../models/User');
2
+ const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/jwt');
3
+ const asyncHandler = require('../utils/asyncHandler');
4
+
5
+ // Register new user
6
+ const register = asyncHandler(async (req, res) => {
7
+ const { email, password, name } = req.body;
8
+
9
+ if (!email || !password) {
10
+ return res.status(400).json({ error: 'Email and password are required' });
11
+ }
12
+
13
+ if (password.length < 6) {
14
+ return res.status(400).json({ error: 'Password must be at least 6 characters' });
15
+ }
16
+
17
+ const existingUser = await User.findOne({ email: email.toLowerCase() });
18
+ if (existingUser) {
19
+ return res.status(400).json({ error: 'Email already registered' });
20
+ }
21
+
22
+ const user = new User({
23
+ email: email.toLowerCase(),
24
+ passwordHash: password,
25
+ name
26
+ });
27
+
28
+ await user.save();
29
+
30
+ const token = generateAccessToken(user._id, user.role);
31
+ const refreshToken = generateRefreshToken(user._id);
32
+
33
+ res.status(201).json({
34
+ token,
35
+ refreshToken,
36
+ user: user.toJSON()
37
+ });
38
+ });
39
+
40
+ // Login user
41
+ const login = asyncHandler(async (req, res) => {
42
+ const { email, password } = req.body;
43
+
44
+ if (!email || !password) {
45
+ return res.status(400).json({ error: 'Email and password are required' });
46
+ }
47
+
48
+ const user = await User.findOne({ email: email.toLowerCase() });
49
+ if (!user) {
50
+ return res.status(401).json({ error: 'Invalid email or password' });
51
+ }
52
+
53
+ const isMatch = await user.comparePassword(password);
54
+ if (!isMatch) {
55
+ return res.status(401).json({ error: 'Invalid email or password' });
56
+ }
57
+
58
+ const token = generateAccessToken(user._id, user.role);
59
+ const refreshToken = generateRefreshToken(user._id);
60
+
61
+ res.json({
62
+ token,
63
+ refreshToken,
64
+ user: user.toJSON()
65
+ });
66
+ });
67
+
68
+ // Refresh access token
69
+ const refresh = asyncHandler(async (req, res) => {
70
+ const { refreshToken } = req.body;
71
+
72
+ if (!refreshToken) {
73
+ return res.status(400).json({ error: 'Refresh token required' });
74
+ }
75
+
76
+ const decoded = verifyRefreshToken(refreshToken);
77
+ const newToken = generateAccessToken(decoded.userId);
78
+ const newRefreshToken = generateRefreshToken(decoded.userId);
79
+
80
+ res.json({ token: newToken, refreshToken: newRefreshToken });
81
+ });
82
+
83
+ // Get current user
84
+ const me = asyncHandler(async (req, res) => {
85
+ res.json({ user: req.user.toJSON() });
86
+ });
87
+
88
+ module.exports = {
89
+ register,
90
+ login,
91
+ refresh,
92
+ me
93
+ };