@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.
- package/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const User = require('../models/User');
|
|
2
|
+
const ActivityLog = require('../models/ActivityLog');
|
|
3
|
+
const bcrypt = require('bcryptjs');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const emailService = require('../services/email.service');
|
|
6
|
+
|
|
7
|
+
// Helper function to log activity
|
|
8
|
+
const logActivity = async (userId, action, category, description, metadata = {}, req) => {
|
|
9
|
+
try {
|
|
10
|
+
await ActivityLog.create({
|
|
11
|
+
userId,
|
|
12
|
+
action,
|
|
13
|
+
category,
|
|
14
|
+
description,
|
|
15
|
+
ipAddress: req.ip || req.connection.remoteAddress,
|
|
16
|
+
userAgent: req.get('user-agent'),
|
|
17
|
+
metadata
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Failed to log activity:', error);
|
|
21
|
+
// Don't throw - activity logging should not break the main flow
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// PUT /api/user/profile - Update user profile
|
|
26
|
+
exports.updateProfile = async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const { name, email } = req.body;
|
|
29
|
+
const userId = req.user._id;
|
|
30
|
+
|
|
31
|
+
const updates = {};
|
|
32
|
+
const changedFields = [];
|
|
33
|
+
if (name !== undefined) {
|
|
34
|
+
updates.name = name;
|
|
35
|
+
changedFields.push('name');
|
|
36
|
+
}
|
|
37
|
+
if (email !== undefined) {
|
|
38
|
+
// Check if email is already taken by another user
|
|
39
|
+
const existingUser = await User.findOne({ email, _id: { $ne: userId } });
|
|
40
|
+
if (existingUser) {
|
|
41
|
+
return res.status(400).json({ error: 'Email already in use' });
|
|
42
|
+
}
|
|
43
|
+
updates.email = email;
|
|
44
|
+
changedFields.push('email');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const user = await User.findByIdAndUpdate(
|
|
48
|
+
userId,
|
|
49
|
+
updates,
|
|
50
|
+
{ new: true, runValidators: true }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!user) {
|
|
54
|
+
return res.status(404).json({ error: 'User not found' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Log activity
|
|
58
|
+
await logActivity(
|
|
59
|
+
userId,
|
|
60
|
+
'update_profile',
|
|
61
|
+
'settings',
|
|
62
|
+
`Updated profile: ${changedFields.join(', ')}`,
|
|
63
|
+
{ updatedFields: changedFields },
|
|
64
|
+
req
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
res.json({
|
|
68
|
+
message: 'Profile updated successfully',
|
|
69
|
+
user
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Error updating profile:', error);
|
|
73
|
+
res.status(500).json({ error: 'Failed to update profile' });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// PUT /api/user/password - Change password
|
|
78
|
+
exports.changePassword = async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const { currentPassword, newPassword } = req.body;
|
|
81
|
+
const userId = req.user._id;
|
|
82
|
+
|
|
83
|
+
if (!currentPassword || !newPassword) {
|
|
84
|
+
return res.status(400).json({
|
|
85
|
+
error: 'Current password and new password are required'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate new password strength
|
|
90
|
+
if (newPassword.length < 8) {
|
|
91
|
+
return res.status(400).json({
|
|
92
|
+
error: 'New password must be at least 8 characters long'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const user = await User.findById(userId);
|
|
97
|
+
if (!user) {
|
|
98
|
+
return res.status(404).json({ error: 'User not found' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Verify current password
|
|
102
|
+
const isMatch = await user.comparePassword(currentPassword);
|
|
103
|
+
if (!isMatch) {
|
|
104
|
+
return res.status(401).json({ error: 'Current password is incorrect' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update password (will be hashed by pre-save hook)
|
|
108
|
+
user.passwordHash = newPassword;
|
|
109
|
+
await user.save();
|
|
110
|
+
|
|
111
|
+
// Log activity
|
|
112
|
+
await logActivity(
|
|
113
|
+
userId,
|
|
114
|
+
'change_password',
|
|
115
|
+
'auth',
|
|
116
|
+
'User changed their password',
|
|
117
|
+
{},
|
|
118
|
+
req
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Send confirmation email
|
|
122
|
+
try {
|
|
123
|
+
await emailService.sendPasswordChangedEmail(user.email);
|
|
124
|
+
} catch (emailError) {
|
|
125
|
+
console.error('Failed to send password change email:', emailError);
|
|
126
|
+
// Don't fail the request if email fails
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
res.json({ message: 'Password changed successfully' });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error changing password:', error);
|
|
132
|
+
res.status(500).json({ error: 'Failed to change password' });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// POST /api/user/password-reset-request - Request password reset
|
|
137
|
+
exports.requestPasswordReset = async (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const { email } = req.body;
|
|
140
|
+
|
|
141
|
+
if (!email) {
|
|
142
|
+
return res.status(400).json({ error: 'Email is required' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Always return success message to prevent email enumeration
|
|
146
|
+
const successMessage = 'If an account with that email exists, a password reset link has been sent.';
|
|
147
|
+
|
|
148
|
+
const user = await User.findOne({ email: email.toLowerCase() });
|
|
149
|
+
|
|
150
|
+
if (user) {
|
|
151
|
+
// Generate secure reset token
|
|
152
|
+
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
153
|
+
const hashedToken = crypto.createHash('sha256').update(resetToken).digest('hex');
|
|
154
|
+
|
|
155
|
+
// Set token and expiry (1 hour)
|
|
156
|
+
user.passwordResetToken = hashedToken;
|
|
157
|
+
user.passwordResetExpiry = new Date(Date.now() + 60 * 60 * 1000);
|
|
158
|
+
await user.save();
|
|
159
|
+
|
|
160
|
+
// Log activity
|
|
161
|
+
await logActivity(
|
|
162
|
+
user._id,
|
|
163
|
+
'password_reset_request',
|
|
164
|
+
'auth',
|
|
165
|
+
'User requested password reset',
|
|
166
|
+
{ email: user.email },
|
|
167
|
+
req
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Send reset email with plain token
|
|
171
|
+
try {
|
|
172
|
+
await emailService.sendPasswordResetEmail(user.email, resetToken);
|
|
173
|
+
} catch (emailError) {
|
|
174
|
+
console.error('Failed to send password reset email:', emailError);
|
|
175
|
+
// Still return success to user
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
res.json({ message: successMessage });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Error requesting password reset:', error);
|
|
182
|
+
res.status(500).json({ error: 'Failed to process password reset request' });
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// POST /api/user/password-reset-confirm - Confirm password reset
|
|
187
|
+
exports.confirmPasswordReset = async (req, res) => {
|
|
188
|
+
try {
|
|
189
|
+
const { token, newPassword } = req.body;
|
|
190
|
+
|
|
191
|
+
if (!token || !newPassword) {
|
|
192
|
+
return res.status(400).json({
|
|
193
|
+
error: 'Token and new password are required'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Validate new password strength
|
|
198
|
+
if (newPassword.length < 8) {
|
|
199
|
+
return res.status(400).json({
|
|
200
|
+
error: 'Password must be at least 8 characters long'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Hash the token to compare with stored hash
|
|
205
|
+
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
|
|
206
|
+
|
|
207
|
+
// Find user with valid token
|
|
208
|
+
const user = await User.findOne({
|
|
209
|
+
passwordResetToken: hashedToken,
|
|
210
|
+
passwordResetExpiry: { $gt: Date.now() }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!user) {
|
|
214
|
+
return res.status(400).json({
|
|
215
|
+
error: 'Invalid or expired password reset token'
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Update password (will be hashed by pre-save hook)
|
|
220
|
+
user.passwordHash = newPassword;
|
|
221
|
+
user.passwordResetToken = undefined;
|
|
222
|
+
user.passwordResetExpiry = undefined;
|
|
223
|
+
await user.save();
|
|
224
|
+
|
|
225
|
+
// Log activity
|
|
226
|
+
await logActivity(
|
|
227
|
+
user._id,
|
|
228
|
+
'password_reset_confirm',
|
|
229
|
+
'auth',
|
|
230
|
+
'User completed password reset',
|
|
231
|
+
{},
|
|
232
|
+
req
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Send confirmation email
|
|
236
|
+
try {
|
|
237
|
+
await emailService.sendPasswordChangedEmail(user.email);
|
|
238
|
+
} catch (emailError) {
|
|
239
|
+
console.error('Failed to send password change email:', emailError);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
res.json({ message: 'Your password has been successfully reset' });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Error confirming password reset:', error);
|
|
245
|
+
res.status(500).json({ error: 'Failed to reset password' });
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// DELETE /api/user/account - Delete user account
|
|
250
|
+
exports.deleteAccount = async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const { password } = req.body;
|
|
253
|
+
const userId = req.user._id;
|
|
254
|
+
|
|
255
|
+
if (!password) {
|
|
256
|
+
return res.status(400).json({
|
|
257
|
+
error: 'Password is required for account deletion'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const user = await User.findById(userId);
|
|
262
|
+
if (!user) {
|
|
263
|
+
return res.status(404).json({ error: 'User not found' });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Verify password for re-authentication
|
|
267
|
+
const isMatch = await user.comparePassword(password);
|
|
268
|
+
if (!isMatch) {
|
|
269
|
+
return res.status(401).json({ error: 'Incorrect password' });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const userEmail = user.email;
|
|
273
|
+
|
|
274
|
+
// Log activity before deletion
|
|
275
|
+
await logActivity(
|
|
276
|
+
userId,
|
|
277
|
+
'delete_account',
|
|
278
|
+
'auth',
|
|
279
|
+
'User deleted their account',
|
|
280
|
+
{ email: userEmail },
|
|
281
|
+
req
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Delete the user
|
|
285
|
+
await User.findByIdAndDelete(userId);
|
|
286
|
+
|
|
287
|
+
// Send confirmation email
|
|
288
|
+
try {
|
|
289
|
+
await emailService.sendAccountDeletionEmail(userEmail);
|
|
290
|
+
} catch (emailError) {
|
|
291
|
+
console.error('Failed to send account deletion email:', emailError);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
res.json({ message: 'Account deleted successfully' });
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error('Error deleting account:', error);
|
|
297
|
+
res.status(500).json({ error: 'Failed to delete account' });
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// GET /api/user/settings - Get user settings
|
|
302
|
+
exports.getSettings = async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const userId = req.user._id;
|
|
305
|
+
|
|
306
|
+
const user = await User.findById(userId);
|
|
307
|
+
if (!user) {
|
|
308
|
+
return res.status(404).json({ error: 'User not found' });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
res.json({
|
|
312
|
+
settings: user.settings || {}
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('Error fetching settings:', error);
|
|
316
|
+
res.status(500).json({ error: 'Failed to fetch settings' });
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// PUT /api/user/settings - Update user settings
|
|
321
|
+
exports.updateSettings = async (req, res) => {
|
|
322
|
+
try {
|
|
323
|
+
const userId = req.user._id;
|
|
324
|
+
const newSettings = req.body;
|
|
325
|
+
|
|
326
|
+
if (!newSettings || typeof newSettings !== 'object') {
|
|
327
|
+
return res.status(400).json({ error: 'Settings must be an object' });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const user = await User.findById(userId);
|
|
331
|
+
if (!user) {
|
|
332
|
+
return res.status(404).json({ error: 'User not found' });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Merge new settings with existing settings
|
|
336
|
+
user.settings = {
|
|
337
|
+
...user.settings,
|
|
338
|
+
...newSettings
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
await user.save();
|
|
342
|
+
|
|
343
|
+
// Log activity
|
|
344
|
+
await logActivity(
|
|
345
|
+
userId,
|
|
346
|
+
'update_settings',
|
|
347
|
+
'settings',
|
|
348
|
+
'User updated their settings',
|
|
349
|
+
{ updatedKeys: Object.keys(newSettings) },
|
|
350
|
+
req
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
res.json({
|
|
354
|
+
message: 'Settings updated successfully',
|
|
355
|
+
settings: user.settings
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Error updating settings:', error);
|
|
359
|
+
res.status(500).json({ error: 'Failed to update settings' });
|
|
360
|
+
}
|
|
361
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const User = require('../models/User');
|
|
3
|
+
const Notification = require('../models/Notification');
|
|
4
|
+
const OrganizationMember = require('../models/OrganizationMember');
|
|
5
|
+
const { createAuditEvent, getBasicAuthActor } = require('../services/audit.service');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LIMIT = 50;
|
|
8
|
+
const MAX_LIMIT = 500;
|
|
9
|
+
|
|
10
|
+
function parseLimit(value) {
|
|
11
|
+
const parsed = parseInt(value, 10);
|
|
12
|
+
if (!Number.isFinite(parsed)) return DEFAULT_LIMIT;
|
|
13
|
+
return Math.min(MAX_LIMIT, Math.max(1, parsed));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseOffset(value) {
|
|
17
|
+
const parsed = parseInt(value, 10);
|
|
18
|
+
if (!Number.isFinite(parsed)) return 0;
|
|
19
|
+
return Math.max(0, parsed);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeRegex(str) {
|
|
23
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
exports.listUsers = async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const { q, role, subscriptionStatus, currentPlan, disabled, limit, offset } = req.query;
|
|
29
|
+
|
|
30
|
+
const parsedLimit = parseLimit(limit);
|
|
31
|
+
const parsedOffset = parseOffset(offset);
|
|
32
|
+
|
|
33
|
+
const query = {};
|
|
34
|
+
|
|
35
|
+
if (q) {
|
|
36
|
+
const pattern = escapeRegex(String(q).trim());
|
|
37
|
+
query.$or = [
|
|
38
|
+
{ email: { $regex: pattern, $options: 'i' } },
|
|
39
|
+
{ name: { $regex: pattern, $options: 'i' } },
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (role) {
|
|
44
|
+
query.role = String(role);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (subscriptionStatus) {
|
|
48
|
+
query.subscriptionStatus = String(subscriptionStatus);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (currentPlan) {
|
|
52
|
+
query.currentPlan = String(currentPlan);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (disabled === 'true') {
|
|
56
|
+
query.disabled = true;
|
|
57
|
+
} else if (disabled === 'false') {
|
|
58
|
+
query.disabled = { $ne: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const users = await User.find(query)
|
|
62
|
+
.select('-passwordHash -passwordResetToken -passwordResetExpiry')
|
|
63
|
+
.sort({ createdAt: -1 })
|
|
64
|
+
.limit(parsedLimit)
|
|
65
|
+
.skip(parsedOffset)
|
|
66
|
+
.lean();
|
|
67
|
+
|
|
68
|
+
const total = await User.countDocuments(query);
|
|
69
|
+
|
|
70
|
+
return res.json({
|
|
71
|
+
users,
|
|
72
|
+
pagination: {
|
|
73
|
+
total,
|
|
74
|
+
limit: parsedLimit,
|
|
75
|
+
offset: parsedOffset,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Admin user list error:', error);
|
|
80
|
+
return res.status(500).json({ error: 'Failed to list users' });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.getUser = async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const { id } = req.params;
|
|
87
|
+
|
|
88
|
+
if (!id || !mongoose.Types.ObjectId.isValid(String(id))) {
|
|
89
|
+
return res.status(400).json({ error: 'Invalid user ID' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const user = await User.findById(id)
|
|
93
|
+
.select('-passwordHash -passwordResetToken -passwordResetExpiry')
|
|
94
|
+
.lean();
|
|
95
|
+
|
|
96
|
+
if (!user) {
|
|
97
|
+
return res.status(404).json({ error: 'User not found' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const [notificationCount, orgMembershipCount] = await Promise.all([
|
|
101
|
+
Notification.countDocuments({ userId: user._id }),
|
|
102
|
+
OrganizationMember.countDocuments({ userId: user._id, status: 'active' }),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
return res.json({
|
|
106
|
+
user,
|
|
107
|
+
counts: {
|
|
108
|
+
notifications: notificationCount,
|
|
109
|
+
organizations: orgMembershipCount,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Admin user get error:', error);
|
|
114
|
+
return res.status(500).json({ error: 'Failed to get user' });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
exports.updateUser = async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const { id } = req.params;
|
|
121
|
+
const { name, role, subscriptionStatus, currentPlan } = req.body;
|
|
122
|
+
|
|
123
|
+
if (!id || !mongoose.Types.ObjectId.isValid(String(id))) {
|
|
124
|
+
return res.status(400).json({ error: 'Invalid user ID' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const user = await User.findById(id);
|
|
128
|
+
if (!user) {
|
|
129
|
+
return res.status(404).json({ error: 'User not found' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const before = user.toJSON();
|
|
133
|
+
const actor = getBasicAuthActor(req);
|
|
134
|
+
|
|
135
|
+
if (name !== undefined) {
|
|
136
|
+
user.name = String(name).trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (role !== undefined) {
|
|
140
|
+
if (!['user', 'admin'].includes(String(role))) {
|
|
141
|
+
return res.status(400).json({ error: 'Invalid role. Must be user or admin.' });
|
|
142
|
+
}
|
|
143
|
+
user.role = String(role);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (subscriptionStatus !== undefined) {
|
|
147
|
+
const validStatuses = ['none', 'active', 'cancelled', 'past_due', 'incomplete', 'incomplete_expired', 'trialing', 'unpaid'];
|
|
148
|
+
if (!validStatuses.includes(String(subscriptionStatus))) {
|
|
149
|
+
return res.status(400).json({ error: 'Invalid subscription status' });
|
|
150
|
+
}
|
|
151
|
+
user.subscriptionStatus = String(subscriptionStatus);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (currentPlan !== undefined) {
|
|
155
|
+
const planStr = String(currentPlan).trim();
|
|
156
|
+
if (planStr.length > 100) {
|
|
157
|
+
return res.status(400).json({ error: 'Plan name too long (max 100 chars)' });
|
|
158
|
+
}
|
|
159
|
+
user.currentPlan = planStr || 'free';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await user.save();
|
|
163
|
+
|
|
164
|
+
await createAuditEvent({
|
|
165
|
+
...actor,
|
|
166
|
+
action: 'admin.user.update',
|
|
167
|
+
entityType: 'User',
|
|
168
|
+
entityId: String(user._id),
|
|
169
|
+
before,
|
|
170
|
+
after: user.toJSON(),
|
|
171
|
+
meta: null,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return res.json({ user: user.toJSON() });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Admin user update error:', error);
|
|
177
|
+
return res.status(500).json({ error: 'Failed to update user' });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
exports.disableUser = async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const { id } = req.params;
|
|
184
|
+
|
|
185
|
+
if (!id || !mongoose.Types.ObjectId.isValid(String(id))) {
|
|
186
|
+
return res.status(400).json({ error: 'Invalid user ID' });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const user = await User.findById(id);
|
|
190
|
+
if (!user) {
|
|
191
|
+
return res.status(404).json({ error: 'User not found' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const before = user.toJSON();
|
|
195
|
+
const actor = getBasicAuthActor(req);
|
|
196
|
+
|
|
197
|
+
user.disabled = true;
|
|
198
|
+
await user.save();
|
|
199
|
+
|
|
200
|
+
await createAuditEvent({
|
|
201
|
+
...actor,
|
|
202
|
+
action: 'admin.user.disable',
|
|
203
|
+
entityType: 'User',
|
|
204
|
+
entityId: String(user._id),
|
|
205
|
+
before,
|
|
206
|
+
after: user.toJSON(),
|
|
207
|
+
meta: null,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return res.json({ message: 'User disabled successfully', user: user.toJSON() });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error('Admin user disable error:', error);
|
|
213
|
+
return res.status(500).json({ error: 'Failed to disable user' });
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
exports.enableUser = async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
const { id } = req.params;
|
|
220
|
+
|
|
221
|
+
if (!id || !mongoose.Types.ObjectId.isValid(String(id))) {
|
|
222
|
+
return res.status(400).json({ error: 'Invalid user ID' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const user = await User.findById(id);
|
|
226
|
+
if (!user) {
|
|
227
|
+
return res.status(404).json({ error: 'User not found' });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const before = user.toJSON();
|
|
231
|
+
const actor = getBasicAuthActor(req);
|
|
232
|
+
|
|
233
|
+
user.disabled = false;
|
|
234
|
+
await user.save();
|
|
235
|
+
|
|
236
|
+
await createAuditEvent({
|
|
237
|
+
...actor,
|
|
238
|
+
action: 'admin.user.enable',
|
|
239
|
+
entityType: 'User',
|
|
240
|
+
entityId: String(user._id),
|
|
241
|
+
before,
|
|
242
|
+
after: user.toJSON(),
|
|
243
|
+
meta: null,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return res.json({ message: 'User enabled successfully', user: user.toJSON() });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('Admin user enable error:', error);
|
|
249
|
+
return res.status(500).json({ error: 'Failed to enable user' });
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
exports.getUserStats = async (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const [
|
|
256
|
+
totalUsers,
|
|
257
|
+
adminUsers,
|
|
258
|
+
activeSubscriptions,
|
|
259
|
+
disabledUsers,
|
|
260
|
+
] = await Promise.all([
|
|
261
|
+
User.countDocuments({}),
|
|
262
|
+
User.countDocuments({ role: 'admin' }),
|
|
263
|
+
User.countDocuments({ subscriptionStatus: 'active' }),
|
|
264
|
+
User.countDocuments({ disabled: true }),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
return res.json({
|
|
268
|
+
total: totalUsers,
|
|
269
|
+
admins: adminUsers,
|
|
270
|
+
activeSubscriptions,
|
|
271
|
+
disabled: disabledUsers,
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error('Admin user stats error:', error);
|
|
275
|
+
return res.status(500).json({ error: 'Failed to get user stats' });
|
|
276
|
+
}
|
|
277
|
+
};
|