@plusscommunities/pluss-maintenance-aws 2.0.5-auth.0 → 2.1.0-auth.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-maintenance-aws",
3
- "version": "2.0.5-auth.0",
3
+ "version": "2.1.0-auth.0",
4
4
  "description": "Extension package to enable maintenance on Pluss Communities Platform",
5
5
  "scripts": {
6
6
  "gc": "node ../../tools/gc ./",
@@ -14,21 +14,22 @@
14
14
  "authupload": "npm i && npm i && npm publish --access public --tag auth",
15
15
  "authupload:p": "npm run authpatch && npm run authupload",
16
16
  "upload": "rm -rf .serverless && npm i && npm publish --access public",
17
- "upload:p": "npm run patch && npm run upload"
17
+ "upload:p": "npm run patch && npm run upload",
18
+ "test": "jest tests -i"
18
19
  },
19
- "author": "Phillip Suh",
20
+ "author": "Thorbjorn Kappel Davis",
20
21
  "license": "ISC",
21
22
  "dependencies": {
22
23
  "@aws/dynamodb-auto-marshaller": "^0.7.1",
23
- "@plusscommunities/pluss-core-aws": "2.0.5-auth.0",
24
+ "@plusscommunities/pluss-core-aws": "2.0.7-auth.0",
24
25
  "amazon-cognito-identity-js": "^2.0.19",
25
- "axios": "^1.6.8",
26
26
  "aws-sdk": "^2.1591.0",
27
+ "axios": "^1.6.8",
28
+ "base64-arraybuffer": "^1.0.2",
27
29
  "expo-server-sdk": "^3.0.1",
28
- "get-image-colors": "^1.8.1",
29
30
  "https": "^1.0.0",
30
31
  "lodash": "^4.17.10",
31
- "moment": "^2.22.2",
32
+ "moment": "^2.30.1",
32
33
  "node-fetch": "^2.2.0",
33
34
  "node-jose": "^1.0.0",
34
35
  "nodemailer": "^6.9.12",
@@ -36,14 +37,25 @@
36
37
  "uuid": "^2.0.3"
37
38
  },
38
39
  "devDependencies": {
40
+ "@types/jest": "^26.0.23",
39
41
  "eslint-config-rallycoding": "^3.2.0",
42
+ "jest": "^29.0.0",
43
+ "jest-aws-sdk-mock": "^1.0.2",
40
44
  "serverless-domain-manager": "^3.3.1",
41
45
  "serverless-prune-plugin": "^1.4.1"
42
46
  },
43
47
  "files": [
44
48
  "db/*",
49
+ "integration/*",
50
+ "requests/*",
45
51
  "ticketing/*",
46
52
  "*.js",
47
53
  "package*.json"
48
- ]
54
+ ],
55
+ "jest": {
56
+ "rootDir": "./",
57
+ "moduleNameMapper": {
58
+ "^./testing/(.*)$": "<rootDir>/testing/$1"
59
+ }
60
+ }
49
61
  }
@@ -0,0 +1,100 @@
1
+ const moment = require("moment");
2
+ const editRef = require("@plusscommunities/pluss-core-aws/db/common/editRef");
3
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
4
+ const getUserPreviewFromReq = require("@plusscommunities/pluss-core-aws/helper/getUserPreviewFromReq");
5
+ const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
6
+ const isValidAssignee = require("./helper/isValidAssignee");
7
+ const getUserPreview = require("@plusscommunities/pluss-core-aws/helper/getUserPreview");
8
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
9
+ const publishNotifications = require("@plusscommunities/pluss-core-aws/db/notifications/publishNotifications");
10
+
11
+ module.exports = async (event, data) => {
12
+ const logId = log("assignRequest", "data", data);
13
+ // check required data
14
+ const requiredKeys = ["id", "userId"];
15
+ if (!data || requiredKeys.some((prop) => !data.hasOwnProperty(prop))) {
16
+ log("assignRequest", "InsufficientInput", 422, logId);
17
+ return { status: 422, data: { error: "Insufficient input" } };
18
+ }
19
+
20
+ const request = await getRef("maintenance", "id", data.id);
21
+ log("assignRequest", "request", request, logId);
22
+ log("assignRequest", "site", request.site, logId);
23
+
24
+ // validate authorisation
25
+ const valid = await validateMasterAuth(
26
+ event,
27
+ "maintenanceTracking",
28
+ request.site
29
+ );
30
+ log("assignRequest", "valid", valid, logId);
31
+ if (!valid) {
32
+ const validAssignee = await isValidAssignee(
33
+ event,
34
+ request.site,
35
+ request.AssigneeId
36
+ );
37
+ log("assignRequest", "validAssignee", validAssignee, logId);
38
+ if (!validAssignee) {
39
+ log("assignRequest", "NotAuthorised", 403, logId);
40
+ return { status: 403, data: { error: "Not authorised" } };
41
+ }
42
+ }
43
+
44
+ const user = await getUserPreviewFromReq(event);
45
+ log("assignRequest", "user", user, logId);
46
+ const assignedUser = await getUserPreview(data.userId);
47
+ log("assignRequest", "assignedUser", assignedUser, logId);
48
+
49
+ const changes = {
50
+ history: request.history || [],
51
+ };
52
+
53
+ // Update history
54
+ changes.history.push({
55
+ timestamp: moment.utc().valueOf(),
56
+ EntryType: "assignment",
57
+ user,
58
+ assignedUser,
59
+ });
60
+
61
+ changes.AssigneeId = data.userId;
62
+ changes.Assignee = assignedUser;
63
+
64
+ log("assignRequest", "changes", changes, logId);
65
+
66
+ const result = await editRef("maintenance", "id", data.id, changes);
67
+
68
+ log("assignRequest", "result", result, logId);
69
+
70
+ // send notification to assigned user
71
+ if (user.id !== assignedUser.id) {
72
+ await publishNotifications(
73
+ [assignedUser.id],
74
+ "MaintenanceJobAssigned",
75
+ result.site,
76
+ result.id,
77
+ result,
78
+ true
79
+ );
80
+ }
81
+
82
+ // send notification to previous assignee
83
+ if (request.AssigneeId && result.AssigneeId !== request.AssigneeId) {
84
+ await publishNotifications(
85
+ [request.AssigneeId],
86
+ "MaintenanceJobUnassigned",
87
+ result.site,
88
+ result.id,
89
+ result,
90
+ true
91
+ );
92
+ }
93
+
94
+ return {
95
+ status: 200,
96
+ data: {
97
+ job: result,
98
+ },
99
+ };
100
+ };
@@ -0,0 +1,39 @@
1
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
2
+ const getUsersByPermission = require("@plusscommunities/pluss-core-aws/helper/users/getUsersByPermission");
3
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
4
+
5
+ // Define the new function to get assignees
6
+ module.exports = async (event) => {
7
+ const qParams = event.queryStringParameters || {};
8
+ const logId = log("getAssignees", "Input", qParams);
9
+
10
+ const isAdmin = await validateMasterAuth(
11
+ event,
12
+ "maintenanceTracking",
13
+ qParams.site
14
+ );
15
+ const isAssignee = await validateMasterAuth(
16
+ event,
17
+ "maintenanceAssignment",
18
+ qParams.site
19
+ );
20
+
21
+ const valid = isAdmin || isAssignee;
22
+ log("getRequests", "valid", valid, logId);
23
+ if (!valid) {
24
+ return { status: 403, data: { error: "Not authorised" } };
25
+ }
26
+
27
+ // get assignees
28
+ const assignees = await getUsersByPermission(qParams.site, [
29
+ "maintenanceTracking",
30
+ "maintenanceAssignment",
31
+ ]);
32
+
33
+ log("getAssignees", "AssigneesLength", assignees.length, logId);
34
+
35
+ // compile results
36
+ const results = { Users: assignees };
37
+ log("getAssignees", "Done", true, logId);
38
+ return { status: 200, data: results };
39
+ };
@@ -0,0 +1,99 @@
1
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
2
+ const getSessionUserFromReqAuthKey = require("@plusscommunities/pluss-core-aws/helper/auth/getSessionUserFromReqAuthKey");
3
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
4
+ const validateSiteAccess = require("@plusscommunities/pluss-core-aws/helper/auth/validateSiteAccess");
5
+ const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
6
+
7
+ module.exports = async (event) => {
8
+ const qParams = event.queryStringParameters;
9
+ const logId = log("getRequests", "Params", qParams);
10
+
11
+ // insufficient input
12
+ if (!qParams.site) {
13
+ return { status: 422, data: { error: "Insufficient input" } };
14
+ }
15
+ log("getRequests", "SufficientInput", true, logId);
16
+
17
+ // no access to site
18
+ const valid = await validateSiteAccess(event, qParams.site);
19
+ log("getRequests", "valid", valid, logId);
20
+ if (!valid) {
21
+ return { status: 403, data: { error: "Not authorised" } };
22
+ }
23
+
24
+ // check auth level to determine whether to fetch all requests or only matching requests
25
+ const authorised = await validateMasterAuth(
26
+ event,
27
+ "maintenanceTracking",
28
+ qParams.site
29
+ );
30
+ let assigneeTracking = false;
31
+ if (!authorised) {
32
+ assigneeTracking = await validateMasterAuth(
33
+ event,
34
+ "maintenanceAssignment",
35
+ qParams.site
36
+ );
37
+ }
38
+
39
+ log("getRequests", "authorised", authorised, logId);
40
+ const userId = authorised ? null : await getSessionUserFromReqAuthKey(event);
41
+
42
+ log("getRequests", "userId", userId, logId);
43
+
44
+ const query =
45
+ authorised || assigneeTracking
46
+ ? {
47
+ IndexName: "MaintenanceSiteIndex",
48
+ KeyConditionExpression: "site = :site",
49
+ ExpressionAttributeValues: {
50
+ ":site": qParams.site,
51
+ },
52
+ }
53
+ : {
54
+ IndexName: "MaintenanceSiteUserIdIndex",
55
+ KeyConditionExpression: "site = :site AND userID = :userId",
56
+ ExpressionAttributeValues: {
57
+ ":site": qParams.site,
58
+ ":userId": userId,
59
+ },
60
+ };
61
+ log("getRequests", "query", query, logId);
62
+
63
+ // check whether pagination is applied
64
+ if (qParams.lastKey) {
65
+ try {
66
+ query.ExclusiveStartKey = JSON.parse(qParams.lastKey);
67
+ } catch (e) {}
68
+ }
69
+
70
+ // get jobs
71
+ const result = await indexQuery("maintenance", query);
72
+ let jobs = result.Items;
73
+
74
+ log("getRequests", "LastEvaluatedKey", result.LastEvaluatedKey, logId);
75
+ log("getRequests", "JobsLength", jobs.length, logId);
76
+
77
+ // filter on status
78
+ if (qParams.status) {
79
+ jobs = jobs.filter((j) => qParams.status.includes(j.status));
80
+ log("getRequests", "FilteredOnStatus", jobs.length, logId);
81
+ }
82
+
83
+ // filter on type
84
+ if (qParams.type) {
85
+ jobs = jobs.filter((j) => qParams.type.includes(j.type));
86
+ log("getRequests", "FilteredOnType", jobs.length, logId);
87
+ }
88
+
89
+ // filter to assigned jobs
90
+ if (!authorised && assigneeTracking) {
91
+ jobs = jobs.filter((j) => j.AssigneeId === userId || j.userID === userId);
92
+ log("getRequests", "FilteredOnAssignee", jobs.length, logId);
93
+ }
94
+
95
+ // compile results
96
+ const results = { Items: jobs, LastKey: result.LastEvaluatedKey };
97
+ log("getRequests", "Done", true, logId);
98
+ return { status: 200, data: results };
99
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Checks if the request has permission.
3
+ *
4
+ * @param {Object} event - The api request object.
5
+ * @param {Object} job - The job object.
6
+ * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating if the request has permission.
7
+ */
8
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
9
+ const isValidAssignee = require("./isValidAssignee");
10
+
11
+ module.exports = async (event, job) => {
12
+ const valid = await validateMasterAuth(
13
+ event,
14
+ "maintenanceTracking",
15
+ job.site
16
+ );
17
+ if (valid) {
18
+ return true;
19
+ }
20
+ if (!job.AssigneeId) {
21
+ return false;
22
+ }
23
+ return await isValidAssignee(event, job.site, job.AssigneeId);
24
+ };
@@ -0,0 +1,18 @@
1
+ const getSessionUserFromReqAuthKey = require("@plusscommunities/pluss-core-aws/helper/auth/getSessionUserFromReqAuthKey");
2
+ const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
3
+
4
+ /**
5
+ * Checks whether the logged in user is the assignee as has the assignment permission for that site
6
+ * @param {*} event A request including headers
7
+ * @param {String} site A site id
8
+ * @param {String} userId A user id to check for
9
+ * @returns {Boolean} true if valid
10
+ */
11
+ module.exports = async (event, site, userId) => {
12
+ const valid = await validateMasterAuth(event, "maintenanceAssignment", site);
13
+ if (!valid) {
14
+ return false;
15
+ }
16
+ const loggedInUser = await getSessionUserFromReqAuthKey(event);
17
+ return loggedInUser === userId;
18
+ };
@@ -0,0 +1,80 @@
1
+ const indexQuery = require("@plusscommunities/pluss-core-aws/db/common/indexQuery");
2
+ const editRef = require("@plusscommunities/pluss-core-aws/db/common/editRef");
3
+ const moment = require("moment");
4
+ const { getStrategy } = require("./integration");
5
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
6
+
7
+ const processSingle = async (strategy, item) => {
8
+ // use IDs to refresh request data
9
+ const logId = log("processSingle", "RefreshFromSource", item.ExternalId);
10
+
11
+ // refresh from external source
12
+ await strategy.refreshFromSource(
13
+ item.InternalId,
14
+ item.ExternalId,
15
+ item.TrackedData
16
+ );
17
+
18
+ // save updated timestamp
19
+ await editRef("externalentities", "RowId", item.RowId, {
20
+ LastUpdated: moment().valueOf(),
21
+ });
22
+
23
+ log("processBatch", "Refreshed", item.ExternalId, logId);
24
+ return true;
25
+ };
26
+
27
+ const processBatch = async (strategy, startTime) => {
28
+ const logId = log("processBatch", "StartTime", moment().valueOf());
29
+
30
+ // use index query to fetch IDs of requests
31
+ const query = {
32
+ IndexName: "EntityTypeIndex",
33
+ KeyConditionExpression:
34
+ "EntityType = :entityType AND LastUpdated < :lastUpdate", // only fetch items that haven't been updated in this scan
35
+ ExpressionAttributeValues: {
36
+ ":entityType": strategy.getEntityType(),
37
+ ":lastUpdate": startTime - strategy.getRefreshInterval(),
38
+ },
39
+ Limit: 10,
40
+ ScanIndexForward: false,
41
+ };
42
+ const { Items } = await indexQuery("externalentities", query);
43
+
44
+ const promises = [];
45
+
46
+ Items.forEach((item) => {
47
+ promises.push(processSingle(strategy, item));
48
+ });
49
+
50
+ await Promise.all(promises);
51
+ log("processBatch", "EndOfBatch", Items.length, logId);
52
+ return Items.length;
53
+ };
54
+
55
+ module.exports.scheduleJobImport = async (event, context, callback) => {
56
+ const logId = log("scheduleJobImport", "Start", true);
57
+ // get active integration
58
+ const integrationStrategy = await getStrategy("plussSpace");
59
+ if (!integrationStrategy.isValidIntegration()) {
60
+ log("scheduleJobImport", "Exit", "No valid integration", logId);
61
+ return;
62
+ }
63
+
64
+ const startTime = moment().valueOf();
65
+ const timeout = 3 * 60 * 1000; // 3 minutes
66
+ let noneToProcess = false;
67
+ log("scheduleJobImport", "StartTime", startTime, logId);
68
+
69
+ while (!noneToProcess && moment().valueOf() < startTime + timeout) {
70
+ const logId = log("scheduleJobImport", "StartLoop", moment().valueOf());
71
+ const count = await processBatch(integrationStrategy, startTime);
72
+ log("scheduleJobImport", "Count", count, logId);
73
+ if (!count) {
74
+ log("scheduleJobImport", "noneToProcess", true, logId);
75
+ noneToProcess = true;
76
+ }
77
+ log("scheduleJobImport", "EndLoop", moment().valueOf(), logId);
78
+ }
79
+ log("scheduleJobImport", "End", moment().valueOf(), logId);
80
+ };
package/updateData.js ADDED
@@ -0,0 +1,33 @@
1
+ const { log } = require("@plusscommunities/pluss-core-aws/helper");
2
+ const generateJsonResponse = require("@plusscommunities/pluss-core-aws/helper/generateJsonResponse");
3
+ const { init } = require("@plusscommunities/pluss-core-aws/config");
4
+ const config = require("./config.json");
5
+ const assignRequest = require("./requests/assignRequest");
6
+
7
+ module.exports.updateData = async (event, context, callback) => {
8
+ init(config);
9
+ const action = event.pathParameters.action;
10
+ const data = JSON.parse(event.body);
11
+ const logId = log(action, "Input", data);
12
+
13
+ let response;
14
+
15
+ try {
16
+ switch (action) {
17
+ case "assign":
18
+ response = await assignRequest(event, data);
19
+ break;
20
+ default:
21
+ break;
22
+ }
23
+ } catch (err) {
24
+ log(action, "InternalError", err.toString(), logId);
25
+ if (!response) {
26
+ response = { status: 500, data: { error: "Internal Error" } };
27
+ }
28
+ }
29
+
30
+ log(action, "Response", response, logId);
31
+
32
+ return callback(null, generateJsonResponse(response.status, response.data));
33
+ };
@@ -1,87 +0,0 @@
1
- const uuid = require("uuid");
2
- const moment = require("moment");
3
- const config = require("../config.json");
4
- const { init } = require("@plusscommunities/pluss-core-aws/config");
5
- const { getBody } = require("@plusscommunities/pluss-core-aws/helper");
6
- const generateJsonResponse = require("@plusscommunities/pluss-core-aws/helper/generateJsonResponse");
7
- const updateRef = require("@plusscommunities/pluss-core-aws/db/common/updateRef");
8
- const validateSiteAccess = require("@plusscommunities/pluss-core-aws/helper/auth/validateSiteAccess");
9
- const publishActivity = require("@plusscommunities/pluss-core-aws/db/activity/publishActivity");
10
- const getUserPreviewFromHeader = require("@plusscommunities/pluss-core-aws/helper/getUserPreviewFromHeader");
11
- const {
12
- newTicketEmailTemplate,
13
- plussTeamEmails,
14
- } = require("@plusscommunities/pluss-core-aws/templates/supportTicketEmails");
15
- const sendEmail = require("@plusscommunities/pluss-core-aws/helper/sendEmail");
16
- const { getConfig } = require("@plusscommunities/pluss-core-aws/config");
17
-
18
- const sendTicketEmail = (ticket) => {
19
- const { communityConfig } = getConfig();
20
-
21
- const subject = newTicketEmailTemplate.subject.replace(
22
- "___TITLE___",
23
- ticket.Title
24
- );
25
- const content = newTicketEmailTemplate.content
26
- .replace("___NAME___", ticket.User.displayName)
27
- .replace("___SITE___", ticket.Site)
28
- .replace("___CLIENT___", communityConfig.company)
29
- .replace("___TITLE___", ticket.Title)
30
- .replace("___TEXT___", ticket.Text.replace(/\n/g, " <br /> "));
31
- sendEmail(plussTeamEmails.join(","), subject, content, true);
32
- };
33
-
34
- module.exports.addTicket = (event, context, callback) => {
35
- init(config);
36
- const data = getBody(event);
37
-
38
- if (!data.site || !data.title) {
39
- return callback(
40
- null,
41
- generateJsonResponse(422, { error: "No site, title, or text attached" })
42
- );
43
- }
44
-
45
- validateSiteAccess(event, data.site).then((authorised) => {
46
- if (!authorised) {
47
- console.error("Authorization not valid");
48
- callback(
49
- null,
50
- generateJsonResponse(422, {
51
- error: {
52
- message: "Authorization not valid.",
53
- },
54
- })
55
- );
56
- return;
57
- }
58
-
59
- getUserPreviewFromHeader(event.headers.authkey)
60
- .then((user) => {
61
- const ticket = {
62
- Title: data.title,
63
- Text: data.text,
64
- Attachments: data.attachments,
65
- Site: data.site,
66
- Id: uuid.v1(),
67
- Timestamp: moment.utc().valueOf(),
68
- History: [],
69
- User: user,
70
- Status: "open",
71
- };
72
- updateRef("supporttickets", ticket).then((result) => {
73
- sendTicketEmail(ticket);
74
- publishActivity("AddTicket", data.site, ticket.id, user, {
75
- title: data.title,
76
- });
77
- callback(
78
- null,
79
- generateJsonResponse(200, { success: true, ticket: result })
80
- );
81
- });
82
- })
83
- .catch((error) => {
84
- callback(null, generateJsonResponse(422, { error }));
85
- });
86
- });
87
- };
@@ -1,50 +0,0 @@
1
- const config = require("../config.json");
2
- const { init } = require("@plusscommunities/pluss-core-aws/config");
3
- const { getBody } = require("@plusscommunities/pluss-core-aws/helper");
4
- const generateJsonResponse = require("@plusscommunities/pluss-core-aws/helper/generateJsonResponse");
5
- const deleteRef = require("@plusscommunities/pluss-core-aws/db/common/deleteRef");
6
- const getRef = require("@plusscommunities/pluss-core-aws/db/common/getRef");
7
- const validateMasterAuth = require("@plusscommunities/pluss-core-aws/helper/auth/validateMasterAuth");
8
- const getUserPreviewFromHeader = require("@plusscommunities/pluss-core-aws/helper/getUserPreviewFromHeader");
9
- const publishActivity = require("@plusscommunities/pluss-core-aws/db/activity/publishActivity");
10
-
11
- module.exports.deleteTicket = (event, context, callback) => {
12
- init(config);
13
- const data = getBody(event);
14
-
15
- validateMasterAuth(event, "master", "plussSpace", true).then((authorised) => {
16
- if (!authorised) {
17
- console.error("Authorization not valid");
18
- callback(
19
- null,
20
- generateJsonResponse(422, {
21
- error: {
22
- message: "Authorization not valid.",
23
- },
24
- })
25
- );
26
- return;
27
- }
28
- getRef("supporttickets", "Id", data.id)
29
- .then((ticket) => {
30
- deleteRef("supporttickets", "Id", data.id)
31
- .then(() => {
32
- getUserPreviewFromHeader(event.headers.authkey).then((user) => {
33
- publishActivity("DeleteTicket", ticket.Site, data.id, user, {
34
- title: ticket.Title,
35
- });
36
- });
37
- callback(null, generateJsonResponse(200, { success: true }));
38
- return;
39
- })
40
- .catch((error) => {
41
- callback(null, generateJsonResponse(422, { error }));
42
- return;
43
- });
44
- })
45
- .catch((error) => {
46
- callback(null, generateJsonResponse(422, { error }));
47
- return;
48
- });
49
- });
50
- };