@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,351 @@
|
|
|
1
|
+
// Email service wrapper using Resend
|
|
2
|
+
// Note: Resend package needs to be installed: npm install resend
|
|
3
|
+
|
|
4
|
+
const GlobalSetting = require("../models/GlobalSetting");
|
|
5
|
+
const EmailLog = require("../models/EmailLog");
|
|
6
|
+
|
|
7
|
+
let resendClient = null;
|
|
8
|
+
|
|
9
|
+
// Cache for settings
|
|
10
|
+
const settingsCache = new Map();
|
|
11
|
+
const CACHE_TTL = 60000; // 1 minute
|
|
12
|
+
|
|
13
|
+
// Helper to get setting with cache
|
|
14
|
+
const getSetting = async (key, defaultValue) => {
|
|
15
|
+
const cached = settingsCache.get(key);
|
|
16
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
17
|
+
return cached.value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const setting = await GlobalSetting.findOne({ key }).lean();
|
|
22
|
+
const value = setting ? setting.value : defaultValue;
|
|
23
|
+
settingsCache.set(key, { value, timestamp: Date.now() });
|
|
24
|
+
return value;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`Error fetching setting ${key}:`, error);
|
|
27
|
+
return defaultValue;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Helper to replace template variables
|
|
32
|
+
const replaceTemplateVars = (template, variables) => {
|
|
33
|
+
let result = template;
|
|
34
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
35
|
+
const regex = new RegExp(`{{${key}}}`, "g");
|
|
36
|
+
result = result.replace(regex, value);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Initialize Resend client if API key is available
|
|
42
|
+
const initResend = async () => {
|
|
43
|
+
// Try to get API key from settings first, then fall back to env
|
|
44
|
+
const apiKey = await getSetting("RESEND_API_KEY", process.env.RESEND_API_KEY);
|
|
45
|
+
|
|
46
|
+
if (apiKey && !resendClient) {
|
|
47
|
+
try {
|
|
48
|
+
const { Resend } = require("resend");
|
|
49
|
+
resendClient = new Resend(apiKey);
|
|
50
|
+
console.log("✅ Resend email service initialized");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(
|
|
53
|
+
"⚠️ Resend package not installed. Email functionality will be simulated.",
|
|
54
|
+
);
|
|
55
|
+
console.warn(" Install with: npm install resend");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Initialize on module load
|
|
61
|
+
initResend().catch((err) => console.error("Error initializing Resend:", err));
|
|
62
|
+
|
|
63
|
+
const sendEmail = async ({
|
|
64
|
+
to,
|
|
65
|
+
subject,
|
|
66
|
+
html,
|
|
67
|
+
text,
|
|
68
|
+
from,
|
|
69
|
+
userId,
|
|
70
|
+
type = "other",
|
|
71
|
+
metadata,
|
|
72
|
+
}) => {
|
|
73
|
+
const defaultFrom =
|
|
74
|
+
from ||
|
|
75
|
+
(await getSetting(
|
|
76
|
+
"EMAIL_FROM",
|
|
77
|
+
process.env.EMAIL_FROM || "SaaSBackend <no-reply@resend.dev>",
|
|
78
|
+
));
|
|
79
|
+
const toArray = Array.isArray(to) ? to : [to];
|
|
80
|
+
|
|
81
|
+
// If Resend is not configured, simulate email sending (for development)
|
|
82
|
+
if (!resendClient) {
|
|
83
|
+
console.log("📧 [SIMULATED EMAIL]");
|
|
84
|
+
console.log(" To:", toArray.join(", "));
|
|
85
|
+
console.log(" From:", defaultFrom);
|
|
86
|
+
console.log(" Subject:", subject);
|
|
87
|
+
console.log(
|
|
88
|
+
" Body Preview:",
|
|
89
|
+
html ? html.substring(0, 100) + "..." : "No HTML",
|
|
90
|
+
);
|
|
91
|
+
console.log(" [Email would be sent in production with Resend API key]");
|
|
92
|
+
|
|
93
|
+
// Log simulated email
|
|
94
|
+
try {
|
|
95
|
+
await EmailLog.create({
|
|
96
|
+
userId,
|
|
97
|
+
to: toArray,
|
|
98
|
+
subject,
|
|
99
|
+
type,
|
|
100
|
+
status: "sent",
|
|
101
|
+
metadata: { ...metadata, simulated: true },
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error("Error logging simulated email:", err.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
simulated: true,
|
|
110
|
+
message: "Email simulated (Resend not configured)",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const { data, error } = await resendClient.emails.send({
|
|
116
|
+
from: defaultFrom,
|
|
117
|
+
to: toArray,
|
|
118
|
+
subject,
|
|
119
|
+
html,
|
|
120
|
+
text,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
console.error("❌ Email send error:", error);
|
|
125
|
+
|
|
126
|
+
// Log failure
|
|
127
|
+
await EmailLog.create({
|
|
128
|
+
userId,
|
|
129
|
+
to: toArray,
|
|
130
|
+
subject,
|
|
131
|
+
type,
|
|
132
|
+
status: "failed",
|
|
133
|
+
error: error.message,
|
|
134
|
+
metadata,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
throw new Error(error.message || "Failed to send email");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log("✅ Email sent successfully:", data);
|
|
141
|
+
|
|
142
|
+
// Log success
|
|
143
|
+
await EmailLog.create({
|
|
144
|
+
userId,
|
|
145
|
+
to: toArray,
|
|
146
|
+
subject,
|
|
147
|
+
type,
|
|
148
|
+
providerId: data.id,
|
|
149
|
+
status: "sent",
|
|
150
|
+
metadata,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
data,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error("❌ Error sending email:", error);
|
|
159
|
+
|
|
160
|
+
// Log failure (catch-all)
|
|
161
|
+
try {
|
|
162
|
+
await EmailLog.create({
|
|
163
|
+
userId,
|
|
164
|
+
to: toArray,
|
|
165
|
+
subject,
|
|
166
|
+
type,
|
|
167
|
+
status: "failed",
|
|
168
|
+
error: error.message,
|
|
169
|
+
metadata,
|
|
170
|
+
});
|
|
171
|
+
} catch (logErr) {
|
|
172
|
+
console.error("Error logging failed email:", logErr.message);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const sendPasswordResetEmail = async (email, resetToken) => {
|
|
180
|
+
const frontendUrl = await getSetting(
|
|
181
|
+
"FRONTEND_URL",
|
|
182
|
+
process.env.FRONTEND_URL || "http://localhost:3000",
|
|
183
|
+
);
|
|
184
|
+
const resetUrl = `${frontendUrl}/reset-password?token=${resetToken}`;
|
|
185
|
+
|
|
186
|
+
// Try to get custom template from settings
|
|
187
|
+
const customTemplate = await getSetting("EMAIL_PASSWORD_RESET_HTML", null);
|
|
188
|
+
|
|
189
|
+
let html;
|
|
190
|
+
if (customTemplate) {
|
|
191
|
+
// Use custom template with variable replacement
|
|
192
|
+
html = replaceTemplateVars(customTemplate, { resetUrl });
|
|
193
|
+
} else {
|
|
194
|
+
// Use default template
|
|
195
|
+
html = `
|
|
196
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
197
|
+
<h2>Password Reset Request</h2>
|
|
198
|
+
<p>Hello,</p>
|
|
199
|
+
<p>We received a request to reset your password.</p>
|
|
200
|
+
<p>Click the button below to reset your password:</p>
|
|
201
|
+
<div style="margin: 30px 0;">
|
|
202
|
+
<a href="${resetUrl}"
|
|
203
|
+
style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
|
204
|
+
Reset Password
|
|
205
|
+
</a>
|
|
206
|
+
</div>
|
|
207
|
+
<p>Or copy and paste this link into your browser:</p>
|
|
208
|
+
<p style="color: #666; word-break: break-all;">${resetUrl}</p>
|
|
209
|
+
<p><strong>This link will expire in 1 hour.</strong></p>
|
|
210
|
+
<p>If you didn't request a password reset, you can safely ignore this email.</p>
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const subject = await getSetting(
|
|
216
|
+
"EMAIL_PASSWORD_RESET_SUBJECT",
|
|
217
|
+
"Reset Your Password",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return sendEmail({
|
|
221
|
+
to: email,
|
|
222
|
+
subject,
|
|
223
|
+
html,
|
|
224
|
+
type: "password-reset",
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const sendPasswordChangedEmail = async (email) => {
|
|
229
|
+
const html = `
|
|
230
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
231
|
+
<h2>Password Changed Successfully</h2>
|
|
232
|
+
<p>Hello,</p>
|
|
233
|
+
<p>This is a confirmation that your account password has been changed successfully.</p>
|
|
234
|
+
<p>If you made this change, you can safely ignore this email.</p>
|
|
235
|
+
<p><strong>If you did not make this change, please contact our support team immediately.</strong></p>
|
|
236
|
+
</div>
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
return sendEmail({
|
|
240
|
+
to: email,
|
|
241
|
+
subject: "Password Changed",
|
|
242
|
+
html,
|
|
243
|
+
type: "password-changed",
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const sendAccountDeletionEmail = async (email) => {
|
|
248
|
+
const html = `
|
|
249
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
250
|
+
<h2>Account Deleted</h2>
|
|
251
|
+
<p>Hello,</p>
|
|
252
|
+
<p>Your account has been successfully deleted as requested.</p>
|
|
253
|
+
<p>We're sorry to see you go.</p>
|
|
254
|
+
</div>
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
return sendEmail({
|
|
258
|
+
to: email,
|
|
259
|
+
subject: "Account Deleted",
|
|
260
|
+
html,
|
|
261
|
+
type: "account-deleted",
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const sendWelcomeEmail = async (email, name) => {
|
|
266
|
+
const html = `
|
|
267
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
268
|
+
<h2>Welcome ${name}!</h2>
|
|
269
|
+
<p>Thank you for joining our service. We're excited to have you on board.</p>
|
|
270
|
+
<p>If you have any questions, feel free to reach out to our support team.</p>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
return sendEmail({
|
|
275
|
+
to: email,
|
|
276
|
+
subject: `Welcome ${name}!`,
|
|
277
|
+
html,
|
|
278
|
+
type: "welcome",
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const sendNotificationEmail = async (email, title, message) => {
|
|
283
|
+
const html = `
|
|
284
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
285
|
+
<h2>${title}</h2>
|
|
286
|
+
<p>${message}</p>
|
|
287
|
+
</div>
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
return sendEmail({
|
|
291
|
+
to: email,
|
|
292
|
+
subject: `Notification: ${title}`,
|
|
293
|
+
html,
|
|
294
|
+
type: "notification",
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const sendSubscriptionEmail = async (email, planName, status) => {
|
|
299
|
+
const html = `
|
|
300
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
301
|
+
<h2>Subscription ${status}</h2>
|
|
302
|
+
<p>Plan: ${planName}</p>
|
|
303
|
+
<p>Status: ${status}</p>
|
|
304
|
+
</div>
|
|
305
|
+
`;
|
|
306
|
+
|
|
307
|
+
return sendEmail({
|
|
308
|
+
to: email,
|
|
309
|
+
subject: `Subscription ${status}`,
|
|
310
|
+
html,
|
|
311
|
+
type: "subscription",
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const sendWaitingListEmail = async (email) => {
|
|
316
|
+
const html = `
|
|
317
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
318
|
+
<h2>Welcome to our waiting list!</h2>
|
|
319
|
+
<p>Thanks for joining, ${email}!</p>
|
|
320
|
+
<p>We'll notify you as soon as a spot becomes available.</p>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
return sendEmail({
|
|
325
|
+
to: email,
|
|
326
|
+
subject: "Welcome to our waiting list!",
|
|
327
|
+
html,
|
|
328
|
+
type: "waiting-list",
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const replaceTemplateVariables = (template, variables) => {
|
|
333
|
+
let result = template;
|
|
334
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
335
|
+
const regex = new RegExp(`{{${key}}}`, "g");
|
|
336
|
+
result = result.replace(regex, value);
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
sendEmail,
|
|
343
|
+
sendPasswordResetEmail,
|
|
344
|
+
sendPasswordChangedEmail,
|
|
345
|
+
sendAccountDeletionEmail,
|
|
346
|
+
sendWelcomeEmail,
|
|
347
|
+
sendNotificationEmail,
|
|
348
|
+
sendSubscriptionEmail,
|
|
349
|
+
sendWaitingListEmail,
|
|
350
|
+
replaceTemplateVariables,
|
|
351
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
const ErrorAggregate = require('../models/ErrorAggregate');
|
|
5
|
+
|
|
6
|
+
const SENSITIVE_KEYS = [
|
|
7
|
+
'password',
|
|
8
|
+
'token',
|
|
9
|
+
'secret',
|
|
10
|
+
'authorization',
|
|
11
|
+
'cookie',
|
|
12
|
+
'apikey',
|
|
13
|
+
'api_key',
|
|
14
|
+
'accesstoken',
|
|
15
|
+
'refreshtoken',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
async function getConfig() {
|
|
19
|
+
return {
|
|
20
|
+
errorTrackingEnabled: process.env.ERROR_TRACKING_ENABLED !== 'false',
|
|
21
|
+
errorMaxSamplesPerAggregate: parseInt(process.env.ERROR_MAX_SAMPLES, 10) || 20,
|
|
22
|
+
errorSampleRatePercent: parseInt(process.env.ERROR_SAMPLE_RATE_PERCENT, 10) || 100,
|
|
23
|
+
errorRateLimitPerMinute: parseInt(process.env.ERROR_RATE_LIMIT_PER_MINUTE, 10) || 30,
|
|
24
|
+
errorRateLimitAnonPerMinute: parseInt(process.env.ERROR_RATE_LIMIT_ANON_PER_MINUTE, 10) || 10,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function scrubValue(key, value) {
|
|
29
|
+
const lowerKey = String(key).toLowerCase();
|
|
30
|
+
for (const sensitive of SENSITIVE_KEYS) {
|
|
31
|
+
if (lowerKey.includes(sensitive)) {
|
|
32
|
+
return '[REDACTED]';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scrubObject(obj, depth = 0) {
|
|
39
|
+
if (depth > 5 || !obj || typeof obj !== 'object') return obj;
|
|
40
|
+
if (Array.isArray(obj)) {
|
|
41
|
+
return obj.slice(0, 10).map((item) => scrubObject(item, depth + 1));
|
|
42
|
+
}
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
45
|
+
if (typeof value === 'object' && value !== null) {
|
|
46
|
+
result[key] = scrubObject(value, depth + 1);
|
|
47
|
+
} else {
|
|
48
|
+
result[key] = scrubValue(key, value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeMessage(message) {
|
|
55
|
+
if (!message) return '';
|
|
56
|
+
return String(message)
|
|
57
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
|
|
58
|
+
.replace(/[0-9a-f]{24}/gi, '<OBJECTID>')
|
|
59
|
+
.replace(/\b\d{4,}\b/g, '<NUM>')
|
|
60
|
+
.replace(/\s+/g, ' ')
|
|
61
|
+
.trim()
|
|
62
|
+
.slice(0, 500);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractTopFrame(stack) {
|
|
66
|
+
if (!stack) return null;
|
|
67
|
+
const lines = String(stack).split('\n').slice(1, 4);
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const match = line.match(/at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?/);
|
|
70
|
+
if (match) {
|
|
71
|
+
const fn = match[1] || '<anonymous>';
|
|
72
|
+
const file = match[2].split('/').pop();
|
|
73
|
+
return `${fn}@${file}:${match[3]}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function computeFingerprint({ source, errorName, messageTemplate, topFrame, path, statusBucket }) {
|
|
80
|
+
const parts = [
|
|
81
|
+
source || 'unknown',
|
|
82
|
+
errorName || 'Error',
|
|
83
|
+
messageTemplate || '',
|
|
84
|
+
topFrame || '',
|
|
85
|
+
path || '',
|
|
86
|
+
statusBucket || '',
|
|
87
|
+
];
|
|
88
|
+
const hash = crypto.createHash('sha256').update(parts.join('|')).digest('hex');
|
|
89
|
+
return hash.slice(0, 32);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getStatusBucket(statusCode) {
|
|
93
|
+
if (!statusCode) return null;
|
|
94
|
+
if (statusCode >= 500) return '5xx';
|
|
95
|
+
if (statusCode >= 400) return '4xx';
|
|
96
|
+
if (statusCode >= 300) return '3xx';
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getTodayKey() {
|
|
101
|
+
return new Date().toISOString().slice(0, 10);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let isLogging = false;
|
|
105
|
+
|
|
106
|
+
async function logError(event) {
|
|
107
|
+
if (isLogging) return null;
|
|
108
|
+
isLogging = true;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const config = await getConfig();
|
|
112
|
+
if (!config.errorTrackingEnabled) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (config.errorSampleRatePercent < 100) {
|
|
117
|
+
if (Math.random() * 100 > config.errorSampleRatePercent) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const source = event.source || 'backend';
|
|
123
|
+
const errorName = event.errorName || event.name || 'Error';
|
|
124
|
+
const rawMessage = String(event.message || '').slice(0, 2000);
|
|
125
|
+
const messageTemplate = normalizeMessage(rawMessage);
|
|
126
|
+
const stack = String(event.stack || '').slice(0, 5000);
|
|
127
|
+
const topFrame = extractTopFrame(stack);
|
|
128
|
+
const path = event.request?.path || event.path || '';
|
|
129
|
+
const statusCode = event.request?.statusCode || event.statusCode;
|
|
130
|
+
const statusBucket = getStatusBucket(statusCode);
|
|
131
|
+
|
|
132
|
+
const fingerprint = computeFingerprint({
|
|
133
|
+
source,
|
|
134
|
+
errorName,
|
|
135
|
+
messageTemplate,
|
|
136
|
+
topFrame,
|
|
137
|
+
path,
|
|
138
|
+
statusBucket,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const sample = {
|
|
142
|
+
at: new Date(),
|
|
143
|
+
message: rawMessage,
|
|
144
|
+
stack,
|
|
145
|
+
actor: scrubObject({
|
|
146
|
+
userId: event.actor?.userId || event.userId,
|
|
147
|
+
role: event.actor?.role || event.role,
|
|
148
|
+
ip: event.actor?.ip || event.ip,
|
|
149
|
+
userAgent: event.actor?.userAgent || event.userAgent,
|
|
150
|
+
}),
|
|
151
|
+
request: scrubObject({
|
|
152
|
+
method: event.request?.method || event.method,
|
|
153
|
+
path,
|
|
154
|
+
statusCode,
|
|
155
|
+
requestId: event.request?.requestId || event.requestId,
|
|
156
|
+
}),
|
|
157
|
+
runtime: scrubObject({
|
|
158
|
+
...(event.runtime || {}),
|
|
159
|
+
nodeVersion: process.version,
|
|
160
|
+
hostname: os.hostname(),
|
|
161
|
+
}),
|
|
162
|
+
extra: scrubObject(event.extra || {}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const todayKey = getTodayKey();
|
|
166
|
+
const maxSamples = config.errorMaxSamplesPerAggregate;
|
|
167
|
+
|
|
168
|
+
const result = await ErrorAggregate.findOneAndUpdate(
|
|
169
|
+
{ fingerprint },
|
|
170
|
+
{
|
|
171
|
+
$inc: { countTotal: 1, [`countsByDay.${todayKey}`]: 1 },
|
|
172
|
+
$set: { lastSeenAt: new Date() },
|
|
173
|
+
$setOnInsert: {
|
|
174
|
+
fingerprint,
|
|
175
|
+
source,
|
|
176
|
+
severity: event.severity || 'error',
|
|
177
|
+
errorName,
|
|
178
|
+
errorCode: event.errorCode || event.code,
|
|
179
|
+
messageTemplate,
|
|
180
|
+
topFrame,
|
|
181
|
+
httpStatusBucket: statusBucket,
|
|
182
|
+
firstSeenAt: new Date(),
|
|
183
|
+
status: 'open',
|
|
184
|
+
},
|
|
185
|
+
$push: {
|
|
186
|
+
samples: {
|
|
187
|
+
$each: [sample],
|
|
188
|
+
$slice: -maxSamples,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{ upsert: true, new: true },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
try {
|
|
198
|
+
console.log('[ErrorLogger] Failed to log error:', err.message);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
} finally {
|
|
204
|
+
isLogging = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function logErrorSync(event) {
|
|
209
|
+
setImmediate(() => {
|
|
210
|
+
logError(event).catch(() => {});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
logError,
|
|
216
|
+
logErrorSync,
|
|
217
|
+
getConfig,
|
|
218
|
+
scrubObject,
|
|
219
|
+
normalizeMessage,
|
|
220
|
+
computeFingerprint,
|
|
221
|
+
};
|