@plusscommunities/pluss-core-aws 2.0.26-beta.0 → 2.0.27
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/aws/getDefaultEmailAddress.js +19 -20
- package/aws/getEmailService.js +14 -15
- package/aws/getEmailServiceInfo.js +25 -18
- package/aws/sendEmail.js +9 -8
- package/db/analytics/logAnalyticsActivity.js +50 -18
- package/helper/sendEmail.js +3 -7
- package/package.json +1 -1
|
@@ -2,25 +2,24 @@ const AWS = require("aws-sdk");
|
|
|
2
2
|
|
|
3
3
|
module.exports = (accessKey, secretKey) => {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
5
|
+
// Without explicit credentials the execution role is used, scoped to
|
|
6
|
+
// this client so the global SDK config is never mutated
|
|
7
|
+
const sesv2 =
|
|
8
|
+
accessKey && secretKey
|
|
9
|
+
? new AWS.SESV2({
|
|
10
|
+
accessKeyId: accessKey,
|
|
11
|
+
secretAccessKey: secretKey,
|
|
12
|
+
})
|
|
13
|
+
: new AWS.SESV2();
|
|
14
|
+
const params = { PageSize: 1 };
|
|
15
|
+
sesv2.listEmailIdentities(params, (err, data) => {
|
|
16
|
+
if (err) return reject(err);
|
|
17
|
+
const { EmailIdentities } = data;
|
|
18
|
+
return resolve(
|
|
19
|
+
EmailIdentities && EmailIdentities.length > 0
|
|
20
|
+
? EmailIdentities[0]
|
|
21
|
+
: null,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
25
24
|
});
|
|
26
25
|
};
|
package/aws/getEmailService.js
CHANGED
|
@@ -2,20 +2,19 @@ const AWS = require("aws-sdk");
|
|
|
2
2
|
|
|
3
3
|
module.exports = (accessKey, secretKey) => {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
5
|
+
// Without explicit credentials the execution role is used, scoped to
|
|
6
|
+
// this client so the global SDK config is never mutated
|
|
7
|
+
const sesv2 =
|
|
8
|
+
accessKey && secretKey
|
|
9
|
+
? new AWS.SESV2({
|
|
10
|
+
accessKeyId: accessKey,
|
|
11
|
+
secretAccessKey: secretKey,
|
|
12
|
+
})
|
|
13
|
+
: new AWS.SESV2();
|
|
14
|
+
const params = {};
|
|
15
|
+
sesv2.getAccount(params, (err, data) => {
|
|
16
|
+
if (err) return reject(err);
|
|
17
|
+
return resolve(data);
|
|
18
|
+
});
|
|
20
19
|
});
|
|
21
20
|
};
|
|
@@ -8,26 +8,33 @@ module.exports = async (accessKey, secretKey) => {
|
|
|
8
8
|
sender: null,
|
|
9
9
|
};
|
|
10
10
|
const { emailConfig } = getConfig();
|
|
11
|
-
|
|
12
|
-
console.log("Amazon SES status", {
|
|
13
|
-
service,
|
|
14
|
-
allwoSandbox: emailConfig.allwoSandbox,
|
|
15
|
-
forceFallback: emailConfig.forceFallback,
|
|
16
|
-
});
|
|
11
|
+
// Fallback-only configurations must not require any SES permissions
|
|
17
12
|
if (emailConfig.forceFallback) return result;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
try {
|
|
14
|
+
const service = await getEmailService(accessKey, secretKey);
|
|
15
|
+
console.log("Amazon SES status", {
|
|
16
|
+
service,
|
|
17
|
+
allwoSandbox: emailConfig.allwoSandbox,
|
|
18
|
+
});
|
|
19
|
+
if (
|
|
20
|
+
service &&
|
|
21
|
+
service.SendingEnabled &&
|
|
22
|
+
(emailConfig.allwoSandbox || service.ProductionAccessEnabled) &&
|
|
23
|
+
service.SendQuota.Max24HourSend > service.SendQuota.SentLast24Hours
|
|
24
|
+
) {
|
|
25
|
+
const sender = await getDefaultEmailAddress(accessKey, secretKey);
|
|
26
|
+
console.log("Amazon SES sender", sender);
|
|
27
|
+
// Enabled only once the sender lookup has succeeded, so partial SES
|
|
28
|
+
// permissions degrade to the fallback sender
|
|
29
|
+
result.enabled = true;
|
|
30
|
+
if (sender) {
|
|
31
|
+
result.sender = sender.IdentityName;
|
|
32
|
+
}
|
|
30
33
|
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// A role without the SES grants degrades to the fallback sender
|
|
36
|
+
// instead of failing the send
|
|
37
|
+
console.log("Amazon SES unavailable", e.message || e);
|
|
31
38
|
}
|
|
32
39
|
return result;
|
|
33
40
|
};
|
package/aws/sendEmail.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
const AWS = require("aws-sdk");
|
|
2
|
-
const { getConfig } = require("../config");
|
|
3
2
|
|
|
4
3
|
module.exports = (from, to, subject, content, access = null, bcc = null) => {
|
|
5
4
|
return new Promise((resolve, reject) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
// Default to the execution role; explicit credentials are only for
|
|
6
|
+
// cross-account sends. Scoped to this client so the global SDK config
|
|
7
|
+
// is never mutated
|
|
8
|
+
const sesv2 = access
|
|
9
|
+
? new AWS.SESV2({
|
|
10
|
+
accessKeyId: access.key,
|
|
11
|
+
secretAccessKey: access.secret,
|
|
12
|
+
})
|
|
13
|
+
: new AWS.SESV2();
|
|
13
14
|
const params = {
|
|
14
15
|
Content: {
|
|
15
16
|
Simple: {
|
|
@@ -3,7 +3,8 @@ const moment = require("moment");
|
|
|
3
3
|
const checkActivityExists = require("./checkActivityExists");
|
|
4
4
|
const updateRef = require("../common/updateRef");
|
|
5
5
|
const getRole = require("../users/getRole");
|
|
6
|
-
const
|
|
6
|
+
const getUser = require("../users/getUser");
|
|
7
|
+
const { getMultiRowId, getRowId, log } = require("../../helper");
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Creates an analytics entry
|
|
@@ -42,33 +43,56 @@ module.exports = async (
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// 3. user.category doesn't exist on the user object — category (staff/primary/linked)
|
|
51
|
-
// lives on the role record.
|
|
52
|
-
// Centralising here avoids burdening call sites with role resolution and guarantees
|
|
53
|
-
// accuracy (always reads current role, not potentially stale caller-provided values).
|
|
54
|
-
// getRole is a DynamoDB point-read (~5ms) and this runs async, so no user-facing latency impact.
|
|
46
|
+
// Never fail on a missing user — persist with null enrichment.
|
|
47
|
+
const actor = user || {};
|
|
48
|
+
|
|
49
|
+
// Type/category are per-site role facts, so resolve them here — callers only
|
|
50
|
+
// carry minimal user objects.
|
|
55
51
|
let userType = null;
|
|
56
52
|
let userCategory = null;
|
|
53
|
+
let enrichmentFailed = false;
|
|
57
54
|
|
|
58
55
|
try {
|
|
59
|
-
const role = await getRole(site,
|
|
56
|
+
const role = await getRole(site, actor.id);
|
|
60
57
|
if (role) {
|
|
61
58
|
userType = role.type || null;
|
|
62
59
|
userCategory = role.category || null;
|
|
63
60
|
}
|
|
64
61
|
} catch (e) {
|
|
65
|
-
//
|
|
62
|
+
// Log so an outage is distinguishable from a missing role, then fall
|
|
63
|
+
// through to the master fallback.
|
|
64
|
+
enrichmentFailed = true;
|
|
65
|
+
log("logActivity", "RoleLookupFailed", {
|
|
66
|
+
site,
|
|
67
|
+
userId: actor.id || null,
|
|
68
|
+
err: e,
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
//
|
|
69
|
-
|
|
72
|
+
// Master fallback: previews omit type (undefined ≠ the system user's explicit
|
|
73
|
+
// null), so resolve it from the user record.
|
|
74
|
+
let primaryType = actor.type;
|
|
75
|
+
if (!userType && primaryType === undefined && actor.id) {
|
|
76
|
+
try {
|
|
77
|
+
const fullUser = await getUser(actor.id);
|
|
78
|
+
primaryType = fullUser ? fullUser.type : null;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// Best-effort — log so a lookup failure is distinguishable from a
|
|
81
|
+
// genuinely uncategorised user, but never block the activity write.
|
|
82
|
+
enrichmentFailed = true;
|
|
83
|
+
log("logActivity", "EnrichmentLookupFailed", {
|
|
84
|
+
site,
|
|
85
|
+
userId: actor.id,
|
|
86
|
+
err: e,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!userType && primaryType === "master") {
|
|
70
91
|
userType = "master";
|
|
71
|
-
|
|
92
|
+
}
|
|
93
|
+
// Masters count as staff for category filtering.
|
|
94
|
+
if (userType === "master" && !userCategory) {
|
|
95
|
+
userCategory = "staff";
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
// Log the activity
|
|
@@ -78,8 +102,8 @@ module.exports = async (
|
|
|
78
102
|
Timestamp: timestamp || moment.utc().valueOf(),
|
|
79
103
|
EntityType: entityType,
|
|
80
104
|
SubjectId: entityId,
|
|
81
|
-
UserId:
|
|
82
|
-
User: user,
|
|
105
|
+
UserId: actor.id || null,
|
|
106
|
+
User: user || null,
|
|
83
107
|
Site: site,
|
|
84
108
|
EntityId: getRowId(entityId, entityType),
|
|
85
109
|
ActivityId: getMultiRowId([entityId, entityType, actionType]),
|
|
@@ -88,6 +112,14 @@ module.exports = async (
|
|
|
88
112
|
userCategory,
|
|
89
113
|
};
|
|
90
114
|
|
|
115
|
+
// An unresolved lookup failure means enrichment didn't happen — omit the
|
|
116
|
+
// keys so aggregation applies pre-enrichment totals semantics rather than
|
|
117
|
+
// recording a falsely "enriched but uncategorised" event.
|
|
118
|
+
if (enrichmentFailed && userType === null && userCategory === null) {
|
|
119
|
+
delete activity.userType;
|
|
120
|
+
delete activity.userCategory;
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
await updateRef(`analytics`, activity);
|
|
92
124
|
return true;
|
|
93
125
|
};
|
package/helper/sendEmail.js
CHANGED
|
@@ -63,8 +63,7 @@ module.exports = async (
|
|
|
63
63
|
rateLimitId = null,
|
|
64
64
|
} = {},
|
|
65
65
|
) => {
|
|
66
|
-
const { communityConfig,
|
|
67
|
-
getConfig();
|
|
66
|
+
const { communityConfig, emailConfig, accessConfig } = getConfig();
|
|
68
67
|
const now = moment.utc();
|
|
69
68
|
let rateLimitIdToUse;
|
|
70
69
|
|
|
@@ -120,10 +119,7 @@ module.exports = async (
|
|
|
120
119
|
</div>`;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
const sesInfo = await getEmailServiceInfo(
|
|
124
|
-
serverlessConfig.key,
|
|
125
|
-
serverlessConfig.secret,
|
|
126
|
-
);
|
|
122
|
+
const sesInfo = await getEmailServiceInfo();
|
|
127
123
|
|
|
128
124
|
const mailOptions = {
|
|
129
125
|
from: fromEmail || `${communityConfig.name} <${sesInfo.sender}>`,
|
|
@@ -142,7 +138,7 @@ module.exports = async (
|
|
|
142
138
|
mailOptions.to,
|
|
143
139
|
mailOptions.subject,
|
|
144
140
|
mailOptions.html,
|
|
145
|
-
|
|
141
|
+
null,
|
|
146
142
|
mailOptions.bcc,
|
|
147
143
|
);
|
|
148
144
|
log("sendEmail", "Success", { toEmail, subject }, logId);
|
package/package.json
CHANGED