Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 151 additions & 64 deletions Bmore-Responsive.postman_collection.json

Large diffs are not rendered by default.

30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ An API to drive disaster and emergency response systems.
<!-- TOC -->

- [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-)

<!-- /TOC -->

Expand Down Expand Up @@ -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._
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions mail_templates/contact_check_in_html.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h1>{{ emailTitle }}</h1>

<p>{{ emailContents }}</p>

<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 2px;" bgcolor="#ED2939">
<a href="{{ entityLink }}" target="_blank" style="padding: 8px 12px; border: 1px solid #ED2939;border-radius: 2px;font-family: Helvetica, Arial, sans-serif;font-size: 14px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
Check In
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
5 changes: 5 additions & 0 deletions mail_templates/contact_check_in_text.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{ emailTitle }}

{{ emailContents }}

{{ entityLink }}
7 changes: 7 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -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'
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion publiccode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 48 additions & 17 deletions src/email/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
8 changes: 8 additions & 0 deletions src/models/entity-contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ const entityContact = (sequelize, DataTypes) => {
}
}

EntityContact.findByEntityId = async (entityId) => {
const entries = await EntityContact.findAll({
where: {entityId}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this automatically find it by pk?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not for some reason. Not sure what was off about that but it wasn't really worth debugging. When I used findByPk nothing came back.

});

return entries;
}

return EntityContact;
};

Expand Down
29 changes: 2 additions & 27 deletions src/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
);
Expand Down
49 changes: 49 additions & 0 deletions src/routes/contact.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Router } from 'express';
import validator from 'validator';
import email from '../email';
import utils from '../utils';

const router = new Router();
Expand Down Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is only running if entityIds and contactIds is undefined. If that's the case. are they needed in the request body?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's for future planning. Eventually, we're going to allow for passing an array of either or both types of ID's to only send emails to those associations. For now, though, that functionality isn't in there.

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;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});

Expand Down
6 changes: 3 additions & 3 deletions src/routes/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand Down
5 changes: 3 additions & 2 deletions src/routes/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading