@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,134 @@
|
|
|
1
|
+
const objectStorage = require('../objectStorage.service');
|
|
2
|
+
|
|
3
|
+
const { createFsLocalEndpoint } = require('./fsLocal');
|
|
4
|
+
const { createS3Endpoint } = require('./s3');
|
|
5
|
+
const { createSftpEndpoint } = require('./sftp');
|
|
6
|
+
|
|
7
|
+
function normalizeType(raw) {
|
|
8
|
+
return String(raw || '').trim().toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function resolveSourceEndpoint() {
|
|
12
|
+
const active = await objectStorage.getActiveBackend();
|
|
13
|
+
if (active === 's3') {
|
|
14
|
+
const cfg = await objectStorage.getS3Config();
|
|
15
|
+
if (!cfg) {
|
|
16
|
+
const err = new Error('Source S3 is not configured');
|
|
17
|
+
err.code = 'SOURCE_S3_NOT_CONFIGURED';
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
return createS3Endpoint(cfg);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return createFsLocalEndpoint({ baseDir: process.env.UPLOAD_DIR || 'uploads' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function resolveTargetEndpointFromEnvConfig(envCfg) {
|
|
27
|
+
const target = envCfg?.assets?.target || null;
|
|
28
|
+
if (!target) return null;
|
|
29
|
+
|
|
30
|
+
const type = normalizeType(target.type);
|
|
31
|
+
if (type === 'fs_local') {
|
|
32
|
+
return createFsLocalEndpoint({ baseDir: target?.fs?.baseDir || process.env.UPLOAD_DIR || 'uploads' });
|
|
33
|
+
}
|
|
34
|
+
if (type === 'fs_remote') {
|
|
35
|
+
return createSftpEndpoint({
|
|
36
|
+
host: target?.ssh?.host,
|
|
37
|
+
port: target?.ssh?.port,
|
|
38
|
+
username: target?.ssh?.username,
|
|
39
|
+
privateKeyPem: target?.ssh?.privateKeyPem,
|
|
40
|
+
passphrase: target?.ssh?.passphrase,
|
|
41
|
+
baseDir: target?.ssh?.baseDir,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (type === 's3') {
|
|
45
|
+
return createS3Endpoint({
|
|
46
|
+
endpoint: target?.s3?.endpoint,
|
|
47
|
+
region: target?.s3?.region,
|
|
48
|
+
bucket: target?.s3?.bucket,
|
|
49
|
+
accessKeyId: target?.s3?.accessKeyId,
|
|
50
|
+
secretAccessKey: target?.s3?.secretAccessKey,
|
|
51
|
+
forcePathStyle: target?.s3?.forcePathStyle,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const err = new Error('Unsupported assets target type');
|
|
56
|
+
err.code = 'UNSUPPORTED_ASSETS_TARGET';
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeKey(key) {
|
|
61
|
+
let s = String(key || '').trim();
|
|
62
|
+
while (s.startsWith('/')) s = s.slice(1);
|
|
63
|
+
if (s.startsWith('uploads/')) s = s.slice('uploads/'.length);
|
|
64
|
+
return s;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function copyKeys({ keys, sourceEndpoint, targetEndpoint, dryRun = false, batchSize = 10 } = {}) {
|
|
68
|
+
const list = Array.isArray(keys) ? keys.map((k) => normalizeKey(k)).filter(Boolean) : [];
|
|
69
|
+
|
|
70
|
+
const result = {
|
|
71
|
+
ok: true,
|
|
72
|
+
requested: list.length,
|
|
73
|
+
copied: 0,
|
|
74
|
+
skipped: 0,
|
|
75
|
+
failed: [],
|
|
76
|
+
dryRun: Boolean(dryRun),
|
|
77
|
+
targetType: targetEndpoint?.type || null,
|
|
78
|
+
sourceType: sourceEndpoint?.type || null,
|
|
79
|
+
details: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < list.length; i += batchSize) {
|
|
83
|
+
const slice = list.slice(i, i + batchSize);
|
|
84
|
+
await Promise.all(slice.map(async (key) => {
|
|
85
|
+
try {
|
|
86
|
+
const obj = await sourceEndpoint.getObject({ key });
|
|
87
|
+
if (!obj?.body) {
|
|
88
|
+
result.failed.push({
|
|
89
|
+
key,
|
|
90
|
+
error: 'Source object not found',
|
|
91
|
+
sourcePath: sourceEndpoint.describeKey ? sourceEndpoint.describeKey(key) : null,
|
|
92
|
+
targetPath: targetEndpoint.describeKey ? targetEndpoint.describeKey(key) : null,
|
|
93
|
+
});
|
|
94
|
+
result.ok = false;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
result.skipped += 1;
|
|
99
|
+
result.details.push({
|
|
100
|
+
key,
|
|
101
|
+
status: 'skipped',
|
|
102
|
+
sourcePath: sourceEndpoint.describeKey ? sourceEndpoint.describeKey(key) : null,
|
|
103
|
+
targetPath: targetEndpoint.describeKey ? targetEndpoint.describeKey(key) : null,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await targetEndpoint.putObject({ key, body: obj.body, contentType: obj.contentType || null });
|
|
108
|
+
result.copied += 1;
|
|
109
|
+
result.details.push({
|
|
110
|
+
key,
|
|
111
|
+
status: 'copied',
|
|
112
|
+
sourcePath: sourceEndpoint.describeKey ? sourceEndpoint.describeKey(key) : null,
|
|
113
|
+
targetPath: targetEndpoint.describeKey ? targetEndpoint.describeKey(key) : null,
|
|
114
|
+
});
|
|
115
|
+
} catch (e) {
|
|
116
|
+
result.failed.push({
|
|
117
|
+
key,
|
|
118
|
+
error: e?.message ? String(e.message) : 'Copy failed',
|
|
119
|
+
sourcePath: sourceEndpoint.describeKey ? sourceEndpoint.describeKey(key) : null,
|
|
120
|
+
targetPath: targetEndpoint.describeKey ? targetEndpoint.describeKey(key) : null,
|
|
121
|
+
});
|
|
122
|
+
result.ok = false;
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
resolveSourceEndpoint,
|
|
132
|
+
resolveTargetEndpointFromEnvConfig,
|
|
133
|
+
copyKeys,
|
|
134
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
async function createS3Endpoint({ endpoint, region, bucket, accessKeyId, secretAccessKey, forcePathStyle } = {}) {
|
|
2
|
+
const safeEndpoint = String(endpoint || '').trim();
|
|
3
|
+
const safeBucket = String(bucket || '').trim();
|
|
4
|
+
const safeAccessKeyId = String(accessKeyId || '').trim();
|
|
5
|
+
const safeSecret = String(secretAccessKey || '').trim();
|
|
6
|
+
|
|
7
|
+
if (!safeEndpoint || !safeBucket || !safeAccessKeyId || !safeSecret) {
|
|
8
|
+
const err = new Error('Invalid S3 endpoint config');
|
|
9
|
+
err.code = 'INVALID_S3_CONFIG';
|
|
10
|
+
throw err;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { S3Client, HeadBucketCommand, CreateBucketCommand, PutObjectCommand, GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
14
|
+
|
|
15
|
+
const client = new S3Client({
|
|
16
|
+
endpoint: safeEndpoint,
|
|
17
|
+
region: String(region || 'us-east-1').trim() || 'us-east-1',
|
|
18
|
+
credentials: {
|
|
19
|
+
accessKeyId: safeAccessKeyId,
|
|
20
|
+
secretAccessKey: safeSecret,
|
|
21
|
+
},
|
|
22
|
+
forcePathStyle: Boolean(forcePathStyle),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function ensureBucket() {
|
|
26
|
+
try {
|
|
27
|
+
await client.send(new HeadBucketCommand({ Bucket: safeBucket }));
|
|
28
|
+
return { ok: true, bucket: safeBucket, ensured: false };
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const status = e?.$metadata?.httpStatusCode;
|
|
31
|
+
if (status !== 404 && e?.name !== 'NotFound') throw e;
|
|
32
|
+
await client.send(new CreateBucketCommand({ Bucket: safeBucket }));
|
|
33
|
+
return { ok: true, bucket: safeBucket, ensured: true };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
type: 's3',
|
|
39
|
+
endpoint: safeEndpoint,
|
|
40
|
+
bucket: safeBucket,
|
|
41
|
+
|
|
42
|
+
async testWritable() {
|
|
43
|
+
await ensureBucket();
|
|
44
|
+
const key = `__migration_test__/${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
45
|
+
await client.send(new PutObjectCommand({ Bucket: safeBucket, Key: key, Body: Buffer.from('ok') }));
|
|
46
|
+
return { ok: true, bucket: safeBucket, endpoint: safeEndpoint };
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async getObject({ key }) {
|
|
50
|
+
const response = await client.send(new GetObjectCommand({ Bucket: safeBucket, Key: key }));
|
|
51
|
+
const chunks = [];
|
|
52
|
+
for await (const chunk of response.Body) chunks.push(chunk);
|
|
53
|
+
return { body: Buffer.concat(chunks), contentType: response.ContentType || null };
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async putObject({ key, body, contentType }) {
|
|
57
|
+
await ensureBucket();
|
|
58
|
+
await client.send(new PutObjectCommand({
|
|
59
|
+
Bucket: safeBucket,
|
|
60
|
+
Key: key,
|
|
61
|
+
Body: body,
|
|
62
|
+
ContentType: contentType || undefined,
|
|
63
|
+
}));
|
|
64
|
+
return { ok: true, bucket: safeBucket, key };
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
describeKey(key) {
|
|
68
|
+
return `s3://${safeBucket}/${key}`;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
createS3Endpoint,
|
|
75
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
async function createSftpEndpoint({ host, port, username, privateKeyPem, passphrase, baseDir } = {}) {
|
|
4
|
+
const safeHost = String(host || '').trim();
|
|
5
|
+
const safeUser = String(username || '').trim();
|
|
6
|
+
const safeKey = String(privateKeyPem || '').trim();
|
|
7
|
+
const safeBaseDir = String(baseDir || '').trim();
|
|
8
|
+
|
|
9
|
+
if (!safeHost || !safeUser || !safeKey || !safeBaseDir) {
|
|
10
|
+
const err = new Error('Invalid SFTP endpoint config');
|
|
11
|
+
err.code = 'INVALID_SFTP_CONFIG';
|
|
12
|
+
throw err;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SftpClient = require('ssh2-sftp-client');
|
|
16
|
+
const client = new SftpClient();
|
|
17
|
+
|
|
18
|
+
const config = {
|
|
19
|
+
host: safeHost,
|
|
20
|
+
port: Number(port) || 22,
|
|
21
|
+
username: safeUser,
|
|
22
|
+
privateKey: safeKey,
|
|
23
|
+
passphrase: passphrase ? String(passphrase) : undefined,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function connect() {
|
|
27
|
+
await client.connect(config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function end() {
|
|
31
|
+
try {
|
|
32
|
+
await client.end();
|
|
33
|
+
} catch (_) {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function remotePath(key) {
|
|
39
|
+
return path.posix.join(safeBaseDir.replace(/\\/g, '/'), String(key || '').replace(/\\/g, '/'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
type: 'fs_remote',
|
|
44
|
+
host: safeHost,
|
|
45
|
+
username: safeUser,
|
|
46
|
+
baseDir: safeBaseDir,
|
|
47
|
+
|
|
48
|
+
async testWritable() {
|
|
49
|
+
await connect();
|
|
50
|
+
try {
|
|
51
|
+
await client.mkdir(safeBaseDir, true);
|
|
52
|
+
const testKey = `.__migration_test__${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
53
|
+
const p = path.posix.join(safeBaseDir.replace(/\\/g, '/'), testKey);
|
|
54
|
+
await client.put(Buffer.from('ok'), p);
|
|
55
|
+
await client.delete(p);
|
|
56
|
+
return { ok: true, host: safeHost, baseDir: safeBaseDir };
|
|
57
|
+
} finally {
|
|
58
|
+
await end();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async getObject({ key }) {
|
|
63
|
+
await connect();
|
|
64
|
+
try {
|
|
65
|
+
const buf = await client.get(remotePath(key));
|
|
66
|
+
const body = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
67
|
+
return { body, contentType: null };
|
|
68
|
+
} finally {
|
|
69
|
+
await end();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async putObject({ key, body }) {
|
|
74
|
+
await connect();
|
|
75
|
+
try {
|
|
76
|
+
await client.mkdir(path.posix.dirname(remotePath(key)), true);
|
|
77
|
+
await client.put(body, remotePath(key));
|
|
78
|
+
return { ok: true, key };
|
|
79
|
+
} finally {
|
|
80
|
+
await end();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
describeKey(key) {
|
|
85
|
+
return remotePath(key);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
createSftpEndpoint,
|
|
92
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const Notification = require('../models/Notification');
|
|
2
|
+
const User = require('../models/User');
|
|
3
|
+
const emailService = require('./email.service');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
async function createNotification({
|
|
7
|
+
userId,
|
|
8
|
+
type,
|
|
9
|
+
title,
|
|
10
|
+
message,
|
|
11
|
+
channel = 'in_app',
|
|
12
|
+
metadata = {},
|
|
13
|
+
sentByAdminId = null,
|
|
14
|
+
broadcastId = null,
|
|
15
|
+
}) {
|
|
16
|
+
const notification = await Notification.create({
|
|
17
|
+
userId,
|
|
18
|
+
type,
|
|
19
|
+
title,
|
|
20
|
+
message,
|
|
21
|
+
channel,
|
|
22
|
+
metadata,
|
|
23
|
+
sentByAdminId,
|
|
24
|
+
broadcastId,
|
|
25
|
+
read: false,
|
|
26
|
+
emailStatus: channel === 'in_app' ? 'skipped' : 'pending',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return notification;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function sendEmailForNotification(notification, userEmail) {
|
|
33
|
+
if (!notification || !userEmail) return notification;
|
|
34
|
+
|
|
35
|
+
if (notification.channel === 'in_app') {
|
|
36
|
+
return notification;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const typeColors = {
|
|
41
|
+
info: '#3B82F6',
|
|
42
|
+
success: '#10B981',
|
|
43
|
+
warning: '#F59E0B',
|
|
44
|
+
error: '#EF4444',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const color = typeColors[notification.type] || '#6B7280';
|
|
48
|
+
|
|
49
|
+
await emailService.sendEmail({
|
|
50
|
+
to: userEmail,
|
|
51
|
+
subject: notification.title,
|
|
52
|
+
html: `
|
|
53
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
54
|
+
<div style="border-left: 4px solid ${color}; padding-left: 16px; margin: 20px 0;">
|
|
55
|
+
<h2 style="margin: 0 0 10px 0; color: #1F2937;">${escapeHtml(notification.title)}</h2>
|
|
56
|
+
<p style="margin: 0; color: #4B5563; line-height: 1.6;">${escapeHtml(notification.message)}</p>
|
|
57
|
+
</div>
|
|
58
|
+
<p style="color: #9CA3AF; font-size: 12px; margin-top: 30px;">
|
|
59
|
+
This is an automated notification from your account.
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
`,
|
|
63
|
+
type: 'notification',
|
|
64
|
+
metadata: {
|
|
65
|
+
notificationId: notification._id.toString(),
|
|
66
|
+
notificationType: notification.type,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
notification.emailStatus = 'sent';
|
|
71
|
+
notification.emailSentAt = new Date();
|
|
72
|
+
await notification.save();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to send notification email:', error);
|
|
75
|
+
notification.emailStatus = 'failed';
|
|
76
|
+
await notification.save();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return notification;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function escapeHtml(str) {
|
|
83
|
+
return String(str || '')
|
|
84
|
+
.replace(/&/g, '&')
|
|
85
|
+
.replace(/</g, '<')
|
|
86
|
+
.replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"')
|
|
88
|
+
.replace(/'/g, ''');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function sendToUser({
|
|
92
|
+
userId,
|
|
93
|
+
type,
|
|
94
|
+
title,
|
|
95
|
+
message,
|
|
96
|
+
channel = 'in_app',
|
|
97
|
+
metadata = {},
|
|
98
|
+
sentByAdminId = null,
|
|
99
|
+
}) {
|
|
100
|
+
const user = await User.findById(userId).lean();
|
|
101
|
+
if (!user) {
|
|
102
|
+
throw new Error('User not found');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const notification = await createNotification({
|
|
106
|
+
userId,
|
|
107
|
+
type,
|
|
108
|
+
title,
|
|
109
|
+
message,
|
|
110
|
+
channel,
|
|
111
|
+
metadata,
|
|
112
|
+
sentByAdminId,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (channel === 'email' || channel === 'both') {
|
|
116
|
+
await sendEmailForNotification(notification, user.email);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return notification;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function sendToUsers({
|
|
123
|
+
userIds,
|
|
124
|
+
type,
|
|
125
|
+
title,
|
|
126
|
+
message,
|
|
127
|
+
channel = 'in_app',
|
|
128
|
+
metadata = {},
|
|
129
|
+
sentByAdminId = null,
|
|
130
|
+
}) {
|
|
131
|
+
const broadcastId = crypto.randomBytes(12).toString('hex');
|
|
132
|
+
const results = [];
|
|
133
|
+
|
|
134
|
+
for (const userId of userIds) {
|
|
135
|
+
try {
|
|
136
|
+
const user = await User.findById(userId).lean();
|
|
137
|
+
if (!user) continue;
|
|
138
|
+
|
|
139
|
+
const notification = await createNotification({
|
|
140
|
+
userId,
|
|
141
|
+
type,
|
|
142
|
+
title,
|
|
143
|
+
message,
|
|
144
|
+
channel,
|
|
145
|
+
metadata,
|
|
146
|
+
sentByAdminId,
|
|
147
|
+
broadcastId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (channel === 'email' || channel === 'both') {
|
|
151
|
+
await sendEmailForNotification(notification, user.email);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
results.push({ userId, success: true, notificationId: notification._id });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`Failed to send notification to user ${userId}:`, error);
|
|
157
|
+
results.push({ userId, success: false, error: error.message });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { broadcastId, results };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function broadcast({
|
|
165
|
+
type,
|
|
166
|
+
title,
|
|
167
|
+
message,
|
|
168
|
+
channel = 'in_app',
|
|
169
|
+
metadata = {},
|
|
170
|
+
sentByAdminId = null,
|
|
171
|
+
userFilter = {},
|
|
172
|
+
}) {
|
|
173
|
+
const users = await User.find(userFilter).select('_id email').lean();
|
|
174
|
+
const userIds = users.map((u) => u._id);
|
|
175
|
+
|
|
176
|
+
return sendToUsers({
|
|
177
|
+
userIds,
|
|
178
|
+
type,
|
|
179
|
+
title,
|
|
180
|
+
message,
|
|
181
|
+
channel,
|
|
182
|
+
metadata,
|
|
183
|
+
sentByAdminId,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function getNotificationStats() {
|
|
188
|
+
const [totalCount, unreadCount, emailPendingCount, emailSentCount, emailFailedCount] = await Promise.all([
|
|
189
|
+
Notification.countDocuments({}),
|
|
190
|
+
Notification.countDocuments({ read: false }),
|
|
191
|
+
Notification.countDocuments({ emailStatus: 'pending' }),
|
|
192
|
+
Notification.countDocuments({ emailStatus: 'sent' }),
|
|
193
|
+
Notification.countDocuments({ emailStatus: 'failed' }),
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
total: totalCount,
|
|
198
|
+
unread: unreadCount,
|
|
199
|
+
emailPending: emailPendingCount,
|
|
200
|
+
emailSent: emailSentCount,
|
|
201
|
+
emailFailed: emailFailedCount,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
createNotification,
|
|
207
|
+
sendEmailForNotification,
|
|
208
|
+
sendToUser,
|
|
209
|
+
sendToUsers,
|
|
210
|
+
broadcast,
|
|
211
|
+
getNotificationStats,
|
|
212
|
+
};
|