@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,149 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const ActionEvent = require('../models/ActionEvent');
|
|
3
|
+
const GlobalSetting = require('../models/GlobalSetting');
|
|
4
|
+
const { verifyAccessToken } = require('../utils/jwt');
|
|
5
|
+
const User = require('../models/User');
|
|
6
|
+
|
|
7
|
+
function parseCookieHeader(cookieHeader) {
|
|
8
|
+
const out = {};
|
|
9
|
+
if (!cookieHeader) return out;
|
|
10
|
+
const parts = cookieHeader.split(';');
|
|
11
|
+
for (const p of parts) {
|
|
12
|
+
const idx = p.indexOf('=');
|
|
13
|
+
if (idx === -1) continue;
|
|
14
|
+
const k = p.slice(0, idx).trim();
|
|
15
|
+
const v = p.slice(idx + 1).trim();
|
|
16
|
+
if (!k) continue;
|
|
17
|
+
out[k] = decodeURIComponent(v);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getMonthRange(date = new Date()) {
|
|
23
|
+
const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 0, 0, 0, 0));
|
|
24
|
+
const end = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, 0, 0, 0, 0));
|
|
25
|
+
return { start, end };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getAnonId(req) {
|
|
29
|
+
const headerAnon = req.get('x-anon-id');
|
|
30
|
+
if (headerAnon) return String(headerAnon).trim();
|
|
31
|
+
|
|
32
|
+
const cookies = parseCookieHeader(req.headers.cookie);
|
|
33
|
+
if (cookies.enbauges_anon_id) return String(cookies.enbauges_anon_id).trim();
|
|
34
|
+
|
|
35
|
+
const bodyAnon = req.body?.anonId;
|
|
36
|
+
if (bodyAnon) return String(bodyAnon).trim();
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function tryAttachUser(req) {
|
|
42
|
+
if (req.user?._id) return;
|
|
43
|
+
const authHeader = req.headers.authorization;
|
|
44
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) return;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const token = authHeader.substring(7);
|
|
48
|
+
const decoded = verifyAccessToken(token);
|
|
49
|
+
const user = await User.findById(decoded.userId);
|
|
50
|
+
if (user) req.user = user;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// optional auth: ignore errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
exports.track = async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
await tryAttachUser(req);
|
|
59
|
+
|
|
60
|
+
const action = String(req.body?.action || '').trim();
|
|
61
|
+
const meta = req.body?.meta ?? null;
|
|
62
|
+
|
|
63
|
+
if (!action) {
|
|
64
|
+
return res.status(400).json({ error: 'action is required' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let actorType = 'anonymous';
|
|
68
|
+
let actorId = getAnonId(req);
|
|
69
|
+
|
|
70
|
+
if (req.user?._id) {
|
|
71
|
+
actorType = 'user';
|
|
72
|
+
actorId = String(req.user._id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!actorId) {
|
|
76
|
+
actorId = crypto.randomUUID();
|
|
77
|
+
res.cookie('enbauges_anon_id', actorId, {
|
|
78
|
+
httpOnly: false,
|
|
79
|
+
sameSite: 'lax',
|
|
80
|
+
maxAge: 1000 * 60 * 60 * 24 * 365,
|
|
81
|
+
path: '/',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await ActionEvent.create({
|
|
86
|
+
action,
|
|
87
|
+
actorType,
|
|
88
|
+
actorId,
|
|
89
|
+
meta,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return res.json({ ok: true, actorType, anonId: actorType === 'anonymous' ? actorId : null });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Metrics track error:', error);
|
|
95
|
+
return res.status(500).json({ error: 'Failed to track' });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
exports.getImpact = async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const { start, end } = getMonthRange(new Date());
|
|
102
|
+
|
|
103
|
+
const activeActorsAgg = await ActionEvent.aggregate([
|
|
104
|
+
{
|
|
105
|
+
$match: {
|
|
106
|
+
createdAt: { $gte: start, $lt: end },
|
|
107
|
+
actorType: { $in: ['user', 'anonymous'] },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
$group: {
|
|
112
|
+
_id: {
|
|
113
|
+
actorType: '$actorType',
|
|
114
|
+
actorId: '$actorId',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{ $count: 'count' },
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const activeUsers = activeActorsAgg?.[0]?.count || 0;
|
|
122
|
+
|
|
123
|
+
const servicesConsulted = await ActionEvent.countDocuments({
|
|
124
|
+
action: 'service_view',
|
|
125
|
+
createdAt: { $gte: start, $lt: end },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const newsletterSetting = await GlobalSetting.findOne({ key: 'newsletter_list' }).lean();
|
|
129
|
+
let newsletterSubscribers = 0;
|
|
130
|
+
if (newsletterSetting?.value) {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(newsletterSetting.value);
|
|
133
|
+
if (Array.isArray(parsed)) newsletterSubscribers = parsed.length;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
newsletterSubscribers = 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return res.json({
|
|
140
|
+
range: { start: start.toISOString(), end: end.toISOString() },
|
|
141
|
+
activeUsers,
|
|
142
|
+
servicesConsulted,
|
|
143
|
+
newsletterSubscribers,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('Metrics impact error:', error);
|
|
147
|
+
return res.status(500).json({ error: 'Failed to compute impact' });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const Notification = require('../models/Notification');
|
|
3
|
+
const User = require('../models/User');
|
|
4
|
+
const notificationService = require('../services/notification.service');
|
|
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
|
+
exports.listNotifications = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { userId, type, channel, emailStatus, broadcastId, limit, offset } = req.query;
|
|
25
|
+
|
|
26
|
+
const parsedLimit = parseLimit(limit);
|
|
27
|
+
const parsedOffset = parseOffset(offset);
|
|
28
|
+
|
|
29
|
+
const query = {};
|
|
30
|
+
|
|
31
|
+
if (userId && mongoose.Types.ObjectId.isValid(String(userId))) {
|
|
32
|
+
query.userId = new mongoose.Types.ObjectId(String(userId));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (type) {
|
|
36
|
+
query.type = String(type);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (channel) {
|
|
40
|
+
query.channel = String(channel);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (emailStatus) {
|
|
44
|
+
query.emailStatus = String(emailStatus);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (broadcastId) {
|
|
48
|
+
query.broadcastId = String(broadcastId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const notifications = await Notification.find(query)
|
|
52
|
+
.populate('userId', 'email name')
|
|
53
|
+
.sort({ createdAt: -1 })
|
|
54
|
+
.limit(parsedLimit)
|
|
55
|
+
.skip(parsedOffset)
|
|
56
|
+
.lean();
|
|
57
|
+
|
|
58
|
+
const total = await Notification.countDocuments(query);
|
|
59
|
+
|
|
60
|
+
return res.json({
|
|
61
|
+
notifications,
|
|
62
|
+
pagination: {
|
|
63
|
+
total,
|
|
64
|
+
limit: parsedLimit,
|
|
65
|
+
offset: parsedOffset,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Admin notification list error:', error);
|
|
70
|
+
return res.status(500).json({ error: 'Failed to list notifications' });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
exports.getNotificationStats = async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const stats = await notificationService.getNotificationStats();
|
|
77
|
+
return res.json(stats);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Admin notification stats error:', error);
|
|
80
|
+
return res.status(500).json({ error: 'Failed to get notification stats' });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.sendNotification = async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const { userIds, type, title, message, channel = 'in_app', metadata = {} } = req.body;
|
|
87
|
+
|
|
88
|
+
if (!type || !title || !message) {
|
|
89
|
+
return res.status(400).json({ error: 'type, title, and message are required' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!['info', 'success', 'warning', 'error'].includes(String(type))) {
|
|
93
|
+
return res.status(400).json({ error: 'Invalid type. Must be info, success, warning, or error.' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!['in_app', 'email', 'both'].includes(String(channel))) {
|
|
97
|
+
return res.status(400).json({ error: 'Invalid channel. Must be in_app, email, or both.' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!Array.isArray(userIds) || userIds.length === 0) {
|
|
101
|
+
return res.status(400).json({ error: 'userIds must be a non-empty array' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const validUserIds = userIds.filter((id) => mongoose.Types.ObjectId.isValid(String(id)));
|
|
105
|
+
if (validUserIds.length === 0) {
|
|
106
|
+
return res.status(400).json({ error: 'No valid user IDs provided' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const actor = getBasicAuthActor(req);
|
|
110
|
+
|
|
111
|
+
const result = await notificationService.sendToUsers({
|
|
112
|
+
userIds: validUserIds,
|
|
113
|
+
type: String(type),
|
|
114
|
+
title: String(title),
|
|
115
|
+
message: String(message),
|
|
116
|
+
channel: String(channel),
|
|
117
|
+
metadata,
|
|
118
|
+
sentByAdminId: actor.actorId,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await createAuditEvent({
|
|
122
|
+
...actor,
|
|
123
|
+
action: 'admin.notification.send',
|
|
124
|
+
entityType: 'Notification',
|
|
125
|
+
entityId: result.broadcastId,
|
|
126
|
+
before: null,
|
|
127
|
+
after: { userCount: validUserIds.length, type, title, channel },
|
|
128
|
+
meta: { broadcastId: result.broadcastId },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return res.status(201).json({
|
|
132
|
+
message: 'Notifications sent',
|
|
133
|
+
broadcastId: result.broadcastId,
|
|
134
|
+
results: result.results,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Admin send notification error:', error);
|
|
138
|
+
return res.status(500).json({ error: 'Failed to send notifications' });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
exports.broadcastNotification = async (req, res) => {
|
|
143
|
+
try {
|
|
144
|
+
const { type, title, message, channel = 'in_app', metadata = {} } = req.body;
|
|
145
|
+
|
|
146
|
+
if (!type || !title || !message) {
|
|
147
|
+
return res.status(400).json({ error: 'type, title, and message are required' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!['info', 'success', 'warning', 'error'].includes(String(type))) {
|
|
151
|
+
return res.status(400).json({ error: 'Invalid type. Must be info, success, warning, or error.' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!['in_app', 'email', 'both'].includes(String(channel))) {
|
|
155
|
+
return res.status(400).json({ error: 'Invalid channel. Must be in_app, email, or both.' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const actor = getBasicAuthActor(req);
|
|
159
|
+
|
|
160
|
+
const result = await notificationService.broadcast({
|
|
161
|
+
type: String(type),
|
|
162
|
+
title: String(title),
|
|
163
|
+
message: String(message),
|
|
164
|
+
channel: String(channel),
|
|
165
|
+
metadata,
|
|
166
|
+
sentByAdminId: actor.actorId,
|
|
167
|
+
userFilter: { disabled: { $ne: true } },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await createAuditEvent({
|
|
171
|
+
...actor,
|
|
172
|
+
action: 'admin.notification.broadcast',
|
|
173
|
+
entityType: 'Notification',
|
|
174
|
+
entityId: result.broadcastId,
|
|
175
|
+
before: null,
|
|
176
|
+
after: { userCount: result.results.length, type, title, channel },
|
|
177
|
+
meta: { broadcastId: result.broadcastId },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return res.status(201).json({
|
|
181
|
+
message: 'Broadcast sent',
|
|
182
|
+
broadcastId: result.broadcastId,
|
|
183
|
+
recipientCount: result.results.length,
|
|
184
|
+
successCount: result.results.filter((r) => r.success).length,
|
|
185
|
+
failCount: result.results.filter((r) => !r.success).length,
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Admin broadcast notification error:', error);
|
|
189
|
+
return res.status(500).json({ error: 'Failed to broadcast notification' });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
exports.deleteNotification = async (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const { id } = req.params;
|
|
196
|
+
|
|
197
|
+
if (!id || !mongoose.Types.ObjectId.isValid(String(id))) {
|
|
198
|
+
return res.status(400).json({ error: 'Invalid notification ID' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const notification = await Notification.findById(id);
|
|
202
|
+
if (!notification) {
|
|
203
|
+
return res.status(404).json({ error: 'Notification not found' });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const before = notification.toObject();
|
|
207
|
+
const actor = getBasicAuthActor(req);
|
|
208
|
+
|
|
209
|
+
await Notification.deleteOne({ _id: id });
|
|
210
|
+
|
|
211
|
+
await createAuditEvent({
|
|
212
|
+
...actor,
|
|
213
|
+
action: 'admin.notification.delete',
|
|
214
|
+
entityType: 'Notification',
|
|
215
|
+
entityId: String(id),
|
|
216
|
+
before,
|
|
217
|
+
after: null,
|
|
218
|
+
meta: null,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return res.json({ message: 'Notification deleted' });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Admin notification delete error:', error);
|
|
224
|
+
return res.status(500).json({ error: 'Failed to delete notification' });
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
exports.retryEmailNotification = async (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
const { id } = req.params;
|
|
231
|
+
|
|
232
|
+
if (!id || !mongoose.Types.ObjectId.isValid(String(id))) {
|
|
233
|
+
return res.status(400).json({ error: 'Invalid notification ID' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const notification = await Notification.findById(id).populate('userId', 'email');
|
|
237
|
+
if (!notification) {
|
|
238
|
+
return res.status(404).json({ error: 'Notification not found' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (notification.channel === 'in_app') {
|
|
242
|
+
return res.status(400).json({ error: 'This notification is in-app only' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (notification.emailStatus === 'sent') {
|
|
246
|
+
return res.status(400).json({ error: 'Email already sent' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const userEmail = notification.userId?.email;
|
|
250
|
+
if (!userEmail) {
|
|
251
|
+
return res.status(400).json({ error: 'User email not found' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await notificationService.sendEmailForNotification(notification, userEmail);
|
|
255
|
+
|
|
256
|
+
return res.json({
|
|
257
|
+
message: 'Email retry attempted',
|
|
258
|
+
emailStatus: notification.emailStatus,
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('Admin notification retry error:', error);
|
|
262
|
+
return res.status(500).json({ error: 'Failed to retry email notification' });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const Notification = require('../models/Notification');
|
|
2
|
+
const ActivityLog = require('../models/ActivityLog');
|
|
3
|
+
|
|
4
|
+
exports.getNotifications = async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const { limit = 50, offset = 0, unreadOnly = false } = req.query;
|
|
7
|
+
|
|
8
|
+
const query = { userId: req.user._id };
|
|
9
|
+
if (unreadOnly === 'true') {
|
|
10
|
+
query.read = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const notifications = await Notification.find(query)
|
|
14
|
+
.sort({ createdAt: -1 })
|
|
15
|
+
.limit(parseInt(limit))
|
|
16
|
+
.skip(parseInt(offset))
|
|
17
|
+
.lean();
|
|
18
|
+
|
|
19
|
+
const total = await Notification.countDocuments(query);
|
|
20
|
+
const unreadCount = await Notification.countDocuments({
|
|
21
|
+
userId: req.user._id,
|
|
22
|
+
read: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
res.json({
|
|
26
|
+
notifications,
|
|
27
|
+
pagination: {
|
|
28
|
+
total,
|
|
29
|
+
limit: parseInt(limit),
|
|
30
|
+
offset: parseInt(offset),
|
|
31
|
+
hasMore: total > parseInt(offset) + parseInt(limit)
|
|
32
|
+
},
|
|
33
|
+
unreadCount
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error fetching notifications:', error);
|
|
37
|
+
res.status(500).json({ error: 'Failed to fetch notifications' });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
exports.markNotificationAsRead = async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const { id } = req.params;
|
|
44
|
+
|
|
45
|
+
const notification = await Notification.findOneAndUpdate(
|
|
46
|
+
{ _id: id, userId: req.user._id },
|
|
47
|
+
{ read: true },
|
|
48
|
+
{ new: true }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!notification) {
|
|
52
|
+
return res.status(404).json({ error: 'Notification not found' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
res.json({
|
|
56
|
+
message: 'Notification marked as read',
|
|
57
|
+
notification
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error marking notification as read:', error);
|
|
61
|
+
res.status(500).json({ error: 'Failed to mark notification as read' });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
exports.getActivityLog = async (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
const { limit = 50, offset = 0, category, action } = req.query;
|
|
68
|
+
|
|
69
|
+
const query = { userId: req.user._id };
|
|
70
|
+
if (category) query.category = category;
|
|
71
|
+
if (action) query.action = action;
|
|
72
|
+
|
|
73
|
+
const activities = await ActivityLog.find(query)
|
|
74
|
+
.sort({ createdAt: -1 })
|
|
75
|
+
.limit(parseInt(limit))
|
|
76
|
+
.skip(parseInt(offset))
|
|
77
|
+
.lean();
|
|
78
|
+
|
|
79
|
+
const total = await ActivityLog.countDocuments(query);
|
|
80
|
+
|
|
81
|
+
res.json({
|
|
82
|
+
activities,
|
|
83
|
+
pagination: {
|
|
84
|
+
total,
|
|
85
|
+
limit: parseInt(limit),
|
|
86
|
+
offset: parseInt(offset),
|
|
87
|
+
hasMore: total > parseInt(offset) + parseInt(limit)
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error fetching activity log:', error);
|
|
92
|
+
res.status(500).json({ error: 'Failed to fetch activity log' });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
exports.createActivityLog = async (req, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const { action, category, description, metadata } = req.body;
|
|
99
|
+
|
|
100
|
+
if (!action || !category || !description) {
|
|
101
|
+
return res.status(400).json({
|
|
102
|
+
error: 'action, category, and description are required'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const validCategories = ['auth', 'billing', 'content', 'settings', 'admin', 'other'];
|
|
107
|
+
if (!validCategories.includes(category)) {
|
|
108
|
+
return res.status(400).json({
|
|
109
|
+
error: `Invalid category. Must be one of: ${validCategories.join(', ')}`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const activity = await ActivityLog.create({
|
|
114
|
+
userId: req.user._id,
|
|
115
|
+
action,
|
|
116
|
+
category,
|
|
117
|
+
description,
|
|
118
|
+
ipAddress: req.ip || req.connection.remoteAddress,
|
|
119
|
+
userAgent: req.get('user-agent'),
|
|
120
|
+
metadata: metadata || {}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
res.status(201).json({
|
|
124
|
+
message: 'Activity log created',
|
|
125
|
+
activity
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error creating activity log:', error);
|
|
129
|
+
res.status(500).json({ error: 'Failed to create activity log' });
|
|
130
|
+
}
|
|
131
|
+
};
|