diff --git a/Bmore-Responsive.postman_collection.json b/Bmore-Responsive.postman_collection.json index 7eefd32a..508eaa07 100644 --- a/Bmore-Responsive.postman_collection.json +++ b/Bmore-Responsive.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "c0693306-4e20-4fda-b114-cb59b9057a5c", + "_postman_id": "3be53e97-52ac-405f-b40f-23073f619002", "name": "Bmore-Responsive", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -14,7 +14,7 @@ { "listen": "test", "script": { - "id": "19989e9e-11ee-476a-b986-5be021e89a45", + "id": "eefedb7b-ba34-4e05-8cc4-550978d36b42", "exec": [ "//grab the token and save it into \"token\" env variable", "pm.environment.set(\"token\",pm.response.text());", @@ -68,7 +68,7 @@ { "listen": "test", "script": { - "id": "19a429d8-ad47-4502-ab20-9fda4c54ea87", + "id": "98e1706d-e8ba-4f25-9b3a-28e3477a3bb2", "exec": [ "// grab the id of the first contatc returned for use in subsequent transactions", "var jsonData = pm.response.json();", @@ -110,7 +110,7 @@ { "listen": "test", "script": { - "id": "3908e851-991d-4e9a-8255-ba21abdef273", + "id": "bb50b172-b46b-4767-b477-cb8adf0bdda6", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -156,7 +156,7 @@ { "listen": "test", "script": { - "id": "e6385331-050e-4ae5-94ff-3388f161bb55", + "id": "e7b60b04-969a-4216-9e43-f80967078377", "exec": [ "//get the email of the new user from the request and save it to env variable", "//this will allow deletion of this in the Delete transaction", @@ -214,7 +214,7 @@ { "listen": "test", "script": { - "id": "9d93e390-dc13-42a1-8b78-c683b6c31e58", + "id": "d5de1aa5-dbc8-4d35-a450-786aa0eb23d4", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -261,7 +261,7 @@ { "listen": "test", "script": { - "id": "13bf75ad-caf0-4fa9-afd7-f6d2a23dae15", + "id": "2498d8d3-1234-4047-88f4-6aee2605d8e5", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -306,7 +306,7 @@ { "listen": "test", "script": { - "id": "822834c8-3772-4bd1-9ec8-65e3f441455d", + "id": "df88b7d3-46c0-4a2b-942d-8b5ea908d3b2", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -344,7 +344,7 @@ { "listen": "test", "script": { - "id": "041e221d-c931-4b22-884a-73fefd1c1b80", + "id": "c79b4c2d-6a47-4214-923c-27f3ea5659e6", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -391,7 +391,7 @@ { "listen": "test", "script": { - "id": "db7d2227-79fe-4dc9-bfee-a97248e53e12", + "id": "b66ebdbd-bbce-4b2b-a4e5-853d2c8fae85", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -440,7 +440,7 @@ { "listen": "test", "script": { - "id": "376871ec-6bdf-4315-af89-90886bff9317", + "id": "d64ec855-28e6-4eef-983b-e58d2402c451", "exec": [ "// grab the id of the first contatc returned for use in subsequent transactions", "var jsonData = pm.response.json();", @@ -484,7 +484,7 @@ { "listen": "test", "script": { - "id": "075e31f9-3255-460a-9fa4-b112db37f39a", + "id": "7d3b66de-29cb-4a97-9937-8042d23ec44b", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -523,7 +523,7 @@ { "listen": "test", "script": { - "id": "fa562b7d-4783-4cbb-a06b-e7b774023591", + "id": "21bec72c-9fe8-4afc-b11d-cd837f3f9a36", "exec": [ "//get the 36 character id of the new contact and save it to env variable", "//this will allow deletion of this in the Delete transaction", @@ -568,13 +568,65 @@ }, "response": [] }, + { + "name": "Contact Send", + "event": [ + { + "listen": "test", + "script": { + "id": "b38d3eef-47f0-4f9e-a710-d82c4fef5492", + "exec": [ + "//get the 36 character id of the new contact and save it to env variable", + "//this will allow deletion of this in the Delete transaction", + "pm.environment.set(\"newContactId\", pm.response.text().slice(0,36));", + "", + "//confirm that request returns a success code of 200", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "token", + "type": "text", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"relationshipTitle\": [\"Primary Contact\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/contact/send", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "contact", + "send" + ] + } + }, + "response": [] + }, { "name": "Update Contact", "event": [ { "listen": "test", "script": { - "id": "6f1765bb-101a-40d0-904f-37af9aa96558", + "id": "fc1c434b-cc59-4e91-a823-6860602bd128", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -621,7 +673,7 @@ { "listen": "test", "script": { - "id": "7e59699f-e52f-47ac-b2aa-05fedf731131", + "id": "788ddd37-f714-440f-9103-97fecceec6af", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -660,7 +712,7 @@ { "listen": "prerequest", "script": { - "id": "e50ba8bc-02c6-40ba-ae51-57fa584d8aee", + "id": "a9a6fb59-1c5c-4d20-b3ca-9c3d84a3f4dc", "type": "text/javascript", "exec": [ "" @@ -670,7 +722,7 @@ { "listen": "test", "script": { - "id": "194a84e5-96bd-4efa-afa6-a7f770d99d85", + "id": "50e1126f-2406-420f-b6a1-96e43971b85f", "type": "text/javascript", "exec": [ "" @@ -689,7 +741,7 @@ { "listen": "test", "script": { - "id": "64a0b1b0-b208-4ad5-a1cd-2bed78440b07", + "id": "d5a5284c-53a9-43ca-ba8d-832c9c98be4d", "exec": [ "// grab the id of the first two entities returned for use in subsequent transactions", "var jsonData = pm.response.json();", @@ -732,7 +784,7 @@ { "listen": "test", "script": { - "id": "126b51f5-5c6a-40eb-95b6-b0316f2e2e2e", + "id": "fe787d2f-da5e-4cef-bbbd-9b0104bb9dfc", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -771,7 +823,7 @@ { "listen": "test", "script": { - "id": "76ea05ba-36d2-45b2-acfa-425f1029860d", + "id": "4ff4b107-c439-4432-86b6-b0b5e79ee991", "exec": [ "//get the 36 character id of the new entity and save it to env variable", "//this will allow deletion of this in the Delete transaction", @@ -822,7 +874,7 @@ { "listen": "test", "script": { - "id": "2393125b-9337-436e-ad40-816eed71e98a", + "id": "8cbc3938-6cf0-4faa-8dac-c899288cd72f", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -869,7 +921,7 @@ { "listen": "test", "script": { - "id": "0c54a785-ee1e-4cb0-b9bb-b290937b50ff", + "id": "6ba7b41b-83bf-457e-939c-ec65c809ebc9", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -920,7 +972,7 @@ { "listen": "test", "script": { - "id": "1831285f-51de-45fb-9a68-e3c281951a44", + "id": "c7df8851-5ddf-4567-8caf-16207b1f83f3", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -969,7 +1021,7 @@ { "listen": "test", "script": { - "id": "63c5ec2a-d368-4014-9ffa-2ac4a4bce1a6", + "id": "45cb9cec-1aab-4fa8-9851-62138204c03e", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -1018,7 +1070,7 @@ { "listen": "prerequest", "script": { - "id": "79b9e2be-9773-4ed5-87d9-46fa27c2d92e", + "id": "c5f74aa2-191d-442c-b57f-f6d75fa8b78a", "type": "text/javascript", "exec": [ "" @@ -1028,7 +1080,7 @@ { "listen": "test", "script": { - "id": "5c697ba8-81eb-487b-99f6-ab624dc9d589", + "id": "dab3358a-6e1e-49c5-8177-134315247a9a", "type": "text/javascript", "exec": [ "" @@ -1039,15 +1091,15 @@ "protocolProfileBehavior": {} }, { - "name": "csv", + "name": "unlinks", "item": [ { - "name": "Contact CSV", + "name": "Unlink Contact to Entities", "event": [ { "listen": "test", "script": { - "id": "737b2c37-bff3-45b8-b01a-02fdcca9e012", + "id": "339e72dd-0d08-48d0-ab59-7a0cc3b16799", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -1058,42 +1110,45 @@ } } ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "token", - "value": "{{token}}", - "type": "text" + "type": "text", + "value": "{{token}}" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"entities\": [\n {\n \"id\": \"{{firstEntityId}}\"\n },\n {\n \"id\": \"{{secondEntityId}}\",\n \"title\": \"Owner\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } }, "url": { - "raw": "{{baseUrl}}/csv/Contact", + "raw": "{{baseUrl}}/contact/unlink/{{firstContactId}}", "host": [ "{{baseUrl}}" ], "path": [ - "csv", - "Contact" + "contact", + "unlink", + "{{firstContactId}}" ] } }, "response": [] }, { - "name": "Entity CSV", + "name": "Unlink Entity to Contacts", "event": [ { "listen": "test", "script": { - "id": "8f8f78ba-eb6f-4c06-9939-7e0e7d2af5af", + "id": "8ec628f0-3fb6-4a0c-88eb-7325dbfb6cfc", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -1104,42 +1159,74 @@ } } ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, "request": { - "method": "GET", + "method": "POST", "header": [ { "key": "token", - "value": "{{token}}", - "type": "text" + "type": "text", + "value": "{{token}}" } ], "body": { "mode": "raw", - "raw": "" + "raw": "{\n \"contacts\": [\n {\n \"id\": \"{{firstContactId}}\"\n },\n {\n \"id\": \"{{secondContactId}}\",\n \"title\": \"Owner\"\n }\n ]\n}\n\n", + "options": { + "raw": { + "language": "json" + } + } }, "url": { - "raw": "{{baseUrl}}/csv/Entity", + "raw": "{{baseUrl}}/entity/unlink/{{firstEntityId}}", "host": [ "{{baseUrl}}" ], "path": [ - "csv", - "Entity" + "entity", + "unlink", + "{{firstEntityId}}" ] } }, "response": [] + } + ], + "description": "These calls are exampled on how to create relationships between contacts and entities. These are put in their own folder in postman as they two sample calls require that Get All Contacts and Get All Entities have already been run.", + "event": [ + { + "listen": "prerequest", + "script": { + "id": "1043a0a5-3f65-4f0c-87b4-5cac16ff9bd7", + "type": "text/javascript", + "exec": [ + "" + ] + } }, { - "name": "User CSV", + "listen": "test", + "script": { + "id": "30492179-c737-494a-8db0-97e28436d888", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "protocolProfileBehavior": {} + }, + { + "name": "csv", + "item": [ + { + "name": "Contact CSV", "event": [ { "listen": "test", "script": { - "id": "5b84cbe0-f036-45bf-b973-f9bf869b5cb8", + "id": "21a85667-7ad4-4a68-a2cf-281da224806a", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -1167,25 +1254,25 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/csv/User", + "raw": "{{baseUrl}}/csv/Contact", "host": [ "{{baseUrl}}" ], "path": [ "csv", - "User" + "Contact" ] } }, "response": [] }, { - "name": "UserRole CSV", + "name": "Entity CSV", "event": [ { "listen": "test", "script": { - "id": "7781e1ff-7c1f-4544-a554-063b098e08bb", + "id": "8ec4b7b3-1ac7-4edc-b1d5-0d4ba0456e84", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -1213,13 +1300,13 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/csv/UserRole", + "raw": "{{baseUrl}}/csv/Entity", "host": [ "{{baseUrl}}" ], "path": [ "csv", - "UserRole" + "Entity" ] } }, @@ -1230,7 +1317,7 @@ { "listen": "prerequest", "script": { - "id": "63407654-a4a9-4402-9e72-011815893577", + "id": "29e2cbbb-c919-45c8-b74e-cc4a77de1a38", "type": "text/javascript", "exec": [ "" @@ -1240,7 +1327,7 @@ { "listen": "test", "script": { - "id": "1c17cf82-30be-4bd0-9efb-89e228067f49", + "id": "cc457e20-3ed2-4898-9bbb-fb698eccf2d4", "type": "text/javascript", "exec": [ "" @@ -1256,7 +1343,7 @@ { "listen": "test", "script": { - "id": "ad699e86-0ff1-48e2-96aa-a339d48e14a1", + "id": "7d5b1979-b61c-4d4d-b506-bb880b279875", "exec": [ "//confirm that request returns a success code of 200", "pm.test(\"Status code is 200\", function () {", @@ -1300,7 +1387,7 @@ { "listen": "prerequest", "script": { - "id": "3a7ed56b-06c9-4ed2-80df-1e3499f06ecd", + "id": "79586dee-3afa-4b22-b259-e3e63f4e4e15", "type": "text/javascript", "exec": [ "" @@ -1310,7 +1397,7 @@ { "listen": "test", "script": { - "id": "eef185d7-4e91-4ea5-b251-1383ee21ef28", + "id": "8a795a8f-64a9-4311-bb01-2b3e0a109f80", "type": "text/javascript", "exec": [ "" diff --git a/README.md b/README.md index b6852b7f..04937062 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,22 @@ An API to drive disaster and emergency response systems. - [Bmore Responsive](#bmore-responsive) - - [Documentation](#documentation) - - [API Spec](#api-spec) - - [Database Documentation](#database-documentation) - - [Infrastructure and Deployment](#infrastructure-and-deployment) + - [Documentation](#documentation) + - [API Spec](#api-spec) + - [Database Documentation](#database-documentation) + - [Infrastructure and Deployment](#infrastructure-and-deployment) - [Setup](#setup) - - [Node and Express setup](#node-and-express-setup) - - [Environment variables](#environment-variables) - - [Example .env](#example-env) - - [PostgreSQL](#postgresql) - - [Sequelize](#sequelize) - - [Docker](#docker) - - [docker-compose](#docker-compose) + - [Node and Express setup](#node-and-express-setup) + - [Environment variables](#environment-variables) + - [Example .env](#example-env) + - [PostgreSQL](#postgresql) + - [Sequelize](#sequelize) + - [Docker](#docker) + - [docker-compose](#docker-compose) - [Using this product](#using-this-product) - - [Testing](#testing) + - [Testing](#testing) - [Sources and Links](#sources-and-links) - - [Contributors ✨](#contributors-) + - [Contributors ✨](#contributors-) @@ -86,6 +86,8 @@ The various variables are defined as follows: - `SMTP_PORT` = _optional_ port number for the SMTP server used to send notification emails - `SMTP_USER` = _optional_ username for the SMTP server used to send notification emails - `SMTP_PASSWORD` = _optional_ password for the SMTP server used to send notification emails +- `URL` = _optional_ the URL for your front-end application +- `TEST_EMAIL` = _optional_ the email you wish to send tests to - `BYPASS_LOGIN` = _optional_ Allows you to hit the endpoints locally without having to login. If you wish to bypass the login process during local dev, set this to `true`. _We do not recommend using the default options for PostgreSQL. The above values are provided as examples. It is more secure to create your own credentials._ @@ -104,6 +106,8 @@ JWT_KEY=test123 DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres DATABASE_SCHEMA=public BYPASS_LOGIN=true +URL=http://localhost:8080 +TEST_EMAIL=jason@codeforbaltimore.org ``` ## PostgreSQL diff --git a/mail_templates/contact_check_in_html.njk b/mail_templates/contact_check_in_html.njk new file mode 100644 index 00000000..973756af --- /dev/null +++ b/mail_templates/contact_check_in_html.njk @@ -0,0 +1,19 @@ +

{{ emailTitle }}

+ +

{{ emailContents }}

+ + + + + +
+ + + + +
+ + Check In + +
+
\ No newline at end of file diff --git a/mail_templates/contact_check_in_text.njk b/mail_templates/contact_check_in_text.njk new file mode 100644 index 00000000..0887cafe --- /dev/null +++ b/mail_templates/contact_check_in_text.njk @@ -0,0 +1,5 @@ +{{ emailTitle }} + +{{ emailContents }} + +{{ entityLink }} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..0131c82e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,7 @@ +site_name: Project Template +theme: + name: readthedocs +plugins: + - swagger +extra: + swagger_url: 'https://raw.githubusercontent.com/CodeForBaltimore/Bmore-Responsive/master/swagger.json' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 39dce106..32dc4c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3043,6 +3043,11 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "complexity": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/complexity/-/complexity-0.0.6.tgz", + "integrity": "sha1-pW7g4D9hz0pKeyh6i/HlTYdb/oM=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", diff --git a/package.json b/package.json index c81ecfd8..0dea65f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bmore-responsive", - "version": "1.1.2", + "version": "1.2.0", "description": "An API-driven CRM (Civic Relationship Management) system.", "main": "src/index.js", "directories": { @@ -40,6 +40,7 @@ "casbin": "4.5.0", "casbin-sequelize-adapter": "2.1.0", "chai": "4.2.0", + "complexity": "0.0.6", "cors": "2.8.5", "crypto": "1.0.1", "dotenv": "8.2.0", diff --git a/publiccode.yml b/publiccode.yml index 4229974e..bca8fe43 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -3,7 +3,7 @@ publiccodeYmlVersion: "0.2" name: Bmore-Responsive url: "https://github.com/CodeForBaltimore/Bmore-Responsive.git" landingUrl: "https://github.com/CodeForBaltimore/Bmore-Responsive" -softwareVersion: "1.1.2" +softwareVersion: "1.2.0" releaseDate: "2020-04-06" platforms: - web diff --git a/src/email/index.js b/src/email/index.js index 05cb8e0f..88e1c56a 100644 --- a/src/email/index.js +++ b/src/email/index.js @@ -19,30 +19,61 @@ const transporter = nodemailer.createTransport({ * @param {string} text plain text of the email */ const sendMail = async (to, subject, html, text) => { - let info = await transporter.sendMail({ - from: `"Healthcare Roll Call" <${process.env.SMTP_USER}>`, // sender address - to, // list of receivers - subject, // Subject line - text, // plain text body - html // html body - }); - console.log("Email sent: %s", info.messageId); + try { + let info = await transporter.sendMail({ + from: `"Healthcare Roll Call" <${process.env.SMTP_USER}>`, // sender address + to, // list of receivers + subject, // Subject line + text, // plain text body + html // html body + }); + console.log("Email sent: %s", info.messageId); + } catch (e) { + console.error(e); + } }; /** * Send a forgot password email. * @param {string} userEmail email address of the user we're sending to * @param {string} resetPasswordToken temporary token for the reset password link + * + * @returns {Boolean} */ const sendForgotPassword = async (userEmail, resetPasswordToken) => { - const emailResetLink = `https://healthcarerollcall.org/reset/${resetPasswordToken}`; - await sendMail( - userEmail, - "Password Reset - Healthcare Roll Call", - nunjucks.render("forgot_password_html.njk", { emailResetLink }), - nunjucks.render("forgot_password_text.njk", { emailResetLink }) - ); - return true; + try { + const emailResetLink = `https://healthcarerollcall.org/reset/${resetPasswordToken}`; + await sendMail( + userEmail, + "Password Reset - Healthcare Roll Call", + nunjucks.render("forgot_password_html.njk", { emailResetLink }), + nunjucks.render("forgot_password_text.njk", { emailResetLink }) + ); + return true; + } catch (e) { + console.error(e); + return false; + } }; -export default { sendForgotPassword }; +const sendContactCheckInEmail = async (info) => { + try { + if (process.env.NODE_ENV === 'production' || process.env.TEST_EMAIL !== undefined && process.env.TEST_EMAIL === info.email) { + const entityLink = `${process.env.URL}/checkin/${info.entityId}?token=${info.token}`; + const emailTitle = `${info.entityName} Check In`; + const emailContents = `Hello ${info.name}! It is time to update the status of ${info.entityName}. Please click the link below to check in.` + await sendMail( + info.email, + emailTitle, + nunjucks.render("contact_check_in_html.njk", { emailTitle, emailContents, entityLink }), + nunjucks.render("contact_check_in_text.njk", { emailTitle, emailContents, entityLink }) + ); + console.log(info.email) + return true; + } + } catch (e) { + console.error(e); + } +} + +export default { sendForgotPassword, sendContactCheckInEmail }; diff --git a/src/models/entity-contact.js b/src/models/entity-contact.js index 1293c7bc..4d3b979b 100644 --- a/src/models/entity-contact.js +++ b/src/models/entity-contact.js @@ -48,6 +48,14 @@ const entityContact = (sequelize, DataTypes) => { } } + EntityContact.findByEntityId = async (entityId) => { + const entries = await EntityContact.findAll({ + where: {entityId} + }); + + return entries; + } + return EntityContact; }; diff --git a/src/models/user.js b/src/models/user.js index 44652812..230adfa9 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -58,39 +58,14 @@ const user = (sequelize, DataTypes) => { } }; - /** - * Validates a login token. - * - * @param {String} token The login token from the user. - * - * @return {Boolean} - */ - User.validateToken = async token => { - /** @todo check if it is a token at all */ - if (token) { - try { - const decoded = jwt.verify(token, process.env.JWT_KEY); - const now = new Date(); - if (now.getTime() < decoded.exp * 1000) { - const user = await User.findByPk(decoded.userId); - if (user) { - return user; - } - } - } catch (e) { - console.error(e); - } - } - return false; - }; - User.decodeToken = async token => { return jwt.verify(token, process.env.JWT_KEY); } + /** @todo deprecate this */ User.getToken = async (userId, email, expiresIn = '1d') => { const token = jwt.sign( - {userId, email}, + {userId, email, type: 'user'}, process.env.JWT_KEY, {expiresIn} ); diff --git a/src/routes/contact.js b/src/routes/contact.js index 8a859bd1..7f29a8d6 100644 --- a/src/routes/contact.js +++ b/src/routes/contact.js @@ -1,5 +1,6 @@ import { Router } from 'express'; import validator from 'validator'; +import email from '../email'; import utils from '../utils'; const router = new Router(); @@ -92,6 +93,54 @@ router.post('/', async (req, res) => { return utils.response(res, code, message); }); +// Sends emails to contacts based on body +router.post('/send', async (req, res) => { + let code; + let message; + const emails = []; + + try { + /** @todo allow for passing entity and contact arrays */ + const { entityIds, contactIds, relationshipTitle } = req.body; + + if (entityIds === undefined && contactIds === undefined) { + const whereClause = (relationshipTitle !== undefined) ? {where: {relationshipTitle}} : {}; + const associations = await req.context.models.EntityContact.findAll(whereClause); + + for (const association of associations) { + const contact = await req.context.models.Contact.findById(association.contactId); + + if (contact.email !== null) { + const entity = await req.context.models.Entity.findById(association.entityId); + // short-lived temporary token that only lasts one hour + const temporaryToken = await utils.getToken(contact.id, contact.email[0].address, 'contact'); + + emails.push({ + email: contact.email[0].address, + name: contact.name, + entityName: entity.name, + entityId: association.entityId, + relationshipTitle: association.relationshipTitle, + token: temporaryToken + }); + } + } + } + + emails.forEach(async (e) => { + email.sendContactCheckInEmail(e); + }) + + code = 200; + message = 'contacts emailed'; + } catch (e) { + console.error(e); + code = 500; + } + + return utils.response(res, code, message); +}); + // Updates any contact. router.put('/', async (req, res) => { let code; diff --git a/src/routes/csv.js b/src/routes/csv.js index afa249e3..c6ad4517 100644 --- a/src/routes/csv.js +++ b/src/routes/csv.js @@ -12,7 +12,7 @@ router.get('/:model_type', async (req, res) => { let message; const modelType = req.params.model_type; try { - if(req.context.models.hasOwnProperty(modelType)){ + if(req.context.models.hasOwnProperty(modelType) && modelType !== 'User' && modelType !== 'UserRole'){ //todo add filtering const results = await req.context.models[modelType].findAll({raw:true}); diff --git a/src/routes/entity.js b/src/routes/entity.js index 5992ebf0..6f3c4636 100644 --- a/src/routes/entity.js +++ b/src/routes/entity.js @@ -53,8 +53,8 @@ router.post('/', async (req, res) => { let code; let message; try { - if (req.body.name !== undefined && req.body.name !== '') { - let { name, address, phone, email, checkIn, contacts } = req.body; + if (req.body.name !== undefined && req.body.name !== '' && req.body.type !== undefined && req.body.type !== '') { + let { name, type, address, phone, email, checkIn, contacts } = req.body; if (!checkIn) { checkIn = { @@ -65,7 +65,7 @@ router.post('/', async (req, res) => { } } - const entity = await req.context.models.Entity.create({ name, address, email, phone, checkIn }); + const entity = await req.context.models.Entity.create({ name, type, address, email, phone, checkIn }); if (contacts) { for(const contact of contacts) { const ec = { diff --git a/src/routes/user.js b/src/routes/user.js index 886d05a5..f5e381a1 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -37,6 +37,7 @@ router.post('/login', loginLimiter, async (req, res) => { return utils.response(res, code, message); }); +// Password reset router.post('/reset/:email', loginLimiter, async(req, res) => { let code; let message; @@ -146,7 +147,7 @@ router.post('/', utils.authMiddleware, async (req, res) => { let code; let message; try { - if (validator.isEmail(req.body.email)) { + if (validator.isEmail(req.body.email) && utils.validatePassword(req.body.password)) { const { email, password, roles } = req.body; const user = await req.context.models.User.create({ email: email.toLowerCase(), password }); @@ -192,7 +193,7 @@ router.put('/', utils.authMiddleware, async (req, res) => { const roles = await e.getRolesForUser(req.context.me.email); if (password) { - if (req.context.me.email === email || roles.includes('admin')) { + if (req.context.me.email === email || roles.includes('admin') && utils.validatePassword(password)) { user.password = password; } } diff --git a/src/tests/csv.routes.spec.js b/src/tests/csv.routes.spec.js index 1bcf6c52..9dcd73d6 100644 --- a/src/tests/csv.routes.spec.js +++ b/src/tests/csv.routes.spec.js @@ -14,19 +14,6 @@ describe('CSV Dump Positive Tests', () => { await authed.destroyToken(); }); - it('Positive Test for CSV Dump on User ', (done) => { - request(app) - .get('/csv/User') - .set('Accept', 'application/json') - .set('token', token) - .expect('Content-Type', 'text/html; charset=utf-8') - .expect(200) - .end((err, res) => { - if (err) return done(err); - done(); - }); - }); - it('Positive Test for CSV Dump on Entity', (done) => { request(app) .get('/csv/Entity') diff --git a/src/tests/entity.routes.spec.js b/src/tests/entity.routes.spec.js index d2c12650..dcc8a996 100644 --- a/src/tests/entity.routes.spec.js +++ b/src/tests/entity.routes.spec.js @@ -12,6 +12,7 @@ const entity = { number: (Math.floor(Math.random() * Math.floor(100000000000))).toString() } ], + type: 'Test', email: [ { address: `${randomWords()}@test.test` diff --git a/src/tests/user.routes.spec.js b/src/tests/user.routes.spec.js index e1131ab3..2853ccb4 100644 --- a/src/tests/user.routes.spec.js +++ b/src/tests/user.routes.spec.js @@ -5,7 +5,7 @@ import { Login } from '../utils/login'; import app from '..'; const { expect } = chai; -const user = { email: `${randomWords()}@test.test`, password: randomWords(), roles: ["admin"] }; +const user = { email: `${randomWords()}@test.test`, password: `Abcdefg42!`, roles: ["admin"] }; describe('User positive tests', () => { const authed = new Login(); diff --git a/src/tests/utils.spec.js b/src/tests/utils.spec.js index b54d3b05..7eef8a0e 100644 --- a/src/tests/utils.spec.js +++ b/src/tests/utils.spec.js @@ -9,6 +9,10 @@ describe('Utils Tests', () => { expect(utils.formatTime(1)).to.equal('00:00:01'); done(); }); + it('should return a role', async () => { + const e = await utils.loadCasbin(); + assert.isNotNull(await e.getRolesForUser('homer.simpson@sfpp.com'),'returns roles') + }); }); describe('Utils Login Tests', () => { diff --git a/src/utils/index.js b/src/utils/index.js index 1c1258f6..98cf67cc 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,6 +3,8 @@ import crypto from 'crypto'; import validator from 'validator'; import fs from 'fs'; +import jwt from 'jsonwebtoken'; +import complexity from 'complexity' import { newEnforcer } from 'casbin'; import { SequelizeAdapter } from 'casbin-sequelize-adapter'; @@ -33,9 +35,9 @@ const formatTime = seconds => { * @returns {Object} */ const loadCasbin = async () => { - let dialectOptions; - if (process.env.NODE_ENV === 'production') { - dialectOptions = { + const dialectOptions = (process.env.NODE_ENV === 'production') ? + { + logging: false, ssl: { rejectUnauthorized: true, ca: [rdsCa], @@ -46,14 +48,13 @@ const loadCasbin = async () => { } } } - }; - } + } : { logging: false }; const a = await SequelizeAdapter.newAdapter( - dbUrl(), { + ...dbUrl(), logging: false, dialect: 'postgres', - dialectOptions: dialectOptions + dialectOptions } ); @@ -61,7 +62,7 @@ const loadCasbin = async () => { } /** - * Validates a user login token. + * Checks a user login token. * * This validates a user token. If the token is invalid the request will immediately be rejected back with a 401. * @@ -70,13 +71,24 @@ const loadCasbin = async () => { * * @return {Boolean} */ -const validateToken = async (req) => { - const authorized = await req.context.models.User.validateToken(req.headers.token); - if (authorized) { - req.context.me = authorized; // add user object to context - return true; - } +const validateToken = async req => { + /** @todo check if it is a token at all */ + if (req.headers.token) { + try { + const decoded = jwt.verify(req.headers.token, process.env.JWT_KEY); + const now = new Date(); + if (now.getTime() < decoded.exp * 1000) { + const user = (decoded.type === 'contact') ? await req.context.models.Contact.findById(decoded.userId) : await req.context.models.User.findByPk(decoded.userId); + if (user) { + req.context.me = user; + return true; + } + } + } catch (e) { + console.error(e); + } + } return false; }; @@ -91,7 +103,9 @@ const validateRoles = async (req) => { const e = await loadCasbin(); const { originalUrl: path, method } = req; - const isAllowed = await e.enforce(req.context.me.email, path, method); + /** @todo refactor this... */ + const email = (req.context.me.email[0].address !== undefined) ? req.context.me.email[0].address : req.context.me.email; + const isAllowed = await e.enforce(email, path, method); return isAllowed; } @@ -102,9 +116,9 @@ const validateRoles = async (req) => { * @param {*} res the response object * @param {*} next the next handler in the chain */ -const authMiddleware = async (req, res, next) => { +const authMiddleware = async (req, res, next) => { let authed = false; - + if (process.env.BYPASS_LOGIN) { authed = process.env.BYPASS_LOGIN; } else { @@ -112,7 +126,7 @@ const authMiddleware = async (req, res, next) => { if (authed) { authed = await validateRoles(req); } - } + } if (authed) { next(); @@ -156,6 +170,24 @@ const encryptPassword = (password, salt) => { .digest('hex'); }; +/** + * Generates a JWT + * + * @param {int} userId + * @param {String} email + * @param {String} expiresIn + * + * @returns {String} + */ +const getToken = async (userId, email, type, expiresIn = '1d') => { + const token = jwt.sign( + { userId, email, type }, + process.env.JWT_KEY, + { expiresIn } + ); + return token; +}; + /** * Checks array of emails for validitiy * @@ -171,6 +203,25 @@ const validateEmails = async emails => { return true; } +/** + * Checks the user's new password for complexity + * + * @param {String} pass + * + * @return {Boolean} + */ +const validatePassword = pass => { + const options = { + uppercase: 1, // A through Z + lowercase: 1, // a through z + special: 1, // ! @ # $ & * + digit: 1, // 0 through 9 + min: 8, // minumum number of characters + } + console.log(complexity.check(pass, options)) + return complexity.check(pass, options) +} + /** * Processes model results based on type * @@ -210,7 +261,9 @@ export default { authMiddleware, response, encryptPassword, + getToken, validateEmails, + validatePassword, processResults, dbUrl }; diff --git a/src/utils/login.js b/src/utils/login.js index c484be92..4df65c84 100644 --- a/src/utils/login.js +++ b/src/utils/login.js @@ -7,7 +7,7 @@ import app from ".."; class Login { constructor() { this.role = randomWords(); - this.user = { email: `${randomWords()}@test.test`, password: randomWords(), roles: [this.role] }; + this.user = { email: `${randomWords()}@test.test`, password: `Abcdefg42!`, roles: [this.role] }; this.methods = [ `GET`, `POST`, diff --git a/swagger.json b/swagger.json index 7b68b8b7..16bf3991 100644 --- a/swagger.json +++ b/swagger.json @@ -6,7 +6,7 @@ } ], "info" : { "description" : "An emergency response and contact management API.", - "version" : "1.1.2", + "version" : "1.2.0", "title" : "Bmore Responsive", "contact" : { "email" : "hello@codeforbaltimore.org" @@ -510,7 +510,7 @@ "application/json" : { "schema" : { "type" : "object", - "required" : [ "name" ], + "required" : [ "name", "type" ], "properties" : { "name" : { "type" : "string", @@ -544,63 +544,6 @@ } } }, - "checkIn" : { - "type" : "array", - "items" : { - "type" : "object", - "properties" : { - "updatedAt" : { - "type" : "string", - "format" : "date-time", - "example" : "2020-01-21T13:45:52.348Z" - }, - "status" : { - "type" : "string", - "example" : "Safe" - }, - "UserId" : { - "type" : "string", - "format" : "uuid", - "example" : "4d9721a2-07f8-45ac-9570-682f4774cfa5" - }, - "ContactId" : { - "type" : "string", - "format" : "uuid", - "example" : "abafa852-ecd0-4d57-9083-85f4dfd9c402" - }, - "questionnaire" : { - "type" : "object", - "properties" : { - "id" : { - "type" : "number", - "example" : 1 - }, - "question1" : { - "type" : "string", - "example" : "They have left handed can openers" - }, - "question2" : { - "type" : "boolean", - "example" : false - } - } - }, - "notes" : { - "type" : "string", - "example" : "Everything is okilly dokilly" - } - } - } - }, - "contacts" : { - "type" : "string", - "example" : [ { - "id" : "" - }, { - "id" : "", - "title" : "" - } ] - }, "description" : { "type" : "string", "example" : "Everything for the left handed man, woman, and child!" @@ -753,15 +696,6 @@ } } }, - "contacts" : { - "type" : "string", - "example" : [ { - "id" : "" - }, { - "id" : "", - "title" : "" - } ] - }, "description" : { "type" : "string", "example" : "Everything for the left handed man, woman, and child!" @@ -1277,6 +1211,56 @@ } } }, + "/contact/send" : { + "post" : { + "tags" : [ "contact" ], + "summary" : "sends a check-in email to all contacts", + "description" : "By sending a request to this endpoint, you can send an email to a single contact or all contacts based on entity or contact id. By sending entity ids you will send an email to each contact associated with each entity id passed. By passed contact ids you will send an email to each contact for each entity they are associated with. By passing nothing you will send an email to every contact and every association.", + "parameters" : [ { + "in" : "header", + "name" : "token", + "required" : true, + "schema" : { + "type" : "string", + "example" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE1ODA3NTM0MDUsImV4cCI6MTU4MDgzOTgwNX0.Q6W7Vo6By35yjZBeLKkk96s8LyqIE2G39AG1H3LRD9M" + } + } ], + "requestBody" : { + "description" : "The body of the payload", + "required" : true, + "content" : { + "application/json" : { + "schema" : { + "type" : "object", + "properties" : { + "relationshipTitle" : { + "type" : "array", + "items" : { + "type" : "string", + "example" : "Primary Contact" + } + } + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "contacts emailed" + }, + "401" : { + "description" : "Unauthorized" + }, + "422" : { + "description" : "Invalid input" + }, + "500" : { + "description" : "Server error" + } + } + } + }, "/contact/{contact_id}" : { "get" : { "tags" : [ "contact" ], @@ -1487,7 +1471,7 @@ "get" : { "tags" : [ "csv" ], "summary" : "returns a comma separated list of the model_type requested", - "description" : "By passing the model_type, you are returned a comma separated list of that model_type. Valid model types are Entity, EntityContact, Contact, User, UserRole.", + "description" : "By passing the model_type, you are returned a comma separated list of that model_type. Valid model types are Entity, EntityContact, and Contact.", "parameters" : [ { "in" : "path", "name" : "model_type", @@ -1495,7 +1479,7 @@ "type" : "string" }, "required" : true, - "description" : "type of model you want a csv data dump for. Options are Contact, Entity, EntityContact, User, and UserRole." + "description" : "type of model you want a csv data dump for. Options are Contact, Entity, and EntityContact." }, { "in" : "header", "name" : "token", @@ -1688,14 +1672,6 @@ } } }, - "contacts" : { - "type" : "array", - "items" : { - "type" : "object", - "properties" : null, - "$ref" : "#/components/schemas/ContactItem" - } - }, "description" : { "type" : "string", "example" : "Everything for the left handed man, woman, and child!"