Skip to content

Commit beb3035

Browse files
authored
Merge pull request #77 from CodeForBaltimore/token_updates
Adding support for short-term tokens and forgot password flow
2 parents 515379d + eae5cb1 commit beb3035

File tree

14 files changed

+602
-475
lines changed

14 files changed

+602
-475
lines changed

README.md

Lines changed: 152 additions & 148 deletions
Large diffs are not rendered by default.

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ services:
2020
db:
2121
image: postgres
2222
restart: always
23+
ports:
24+
- '5432:5432'
2325
environment:
2426
- POSTGRES_USER=${DATABASE_USER}
2527
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}

docs/swagger/swagger.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,37 @@
9696
}
9797
}
9898
},
99+
"/user/forgotpassword/{email}" : {
100+
"post" : {
101+
"tags" : [ "user" ],
102+
"summary" : "sends a reset password email",
103+
"description" : "The forgot password endpoint.",
104+
"parameters" : [ {
105+
"in" : "path",
106+
"name" : "email",
107+
"schema" : {
108+
"type" : "string",
109+
"format" : "email"
110+
},
111+
"required" : true,
112+
"description" : "email of the user"
113+
}],
114+
"responses" : {
115+
"200" : {
116+
"description" : "Reset password email sent"
117+
},
118+
"404" : {
119+
"description" : "User not found by email address provided"
120+
},
121+
"422" : {
122+
"description" : "Invalid input"
123+
},
124+
"500" : {
125+
"description" : "Server error"
126+
}
127+
}
128+
}
129+
},
99130
"/user" : {
100131
"post" : {
101132
"tags" : [ "user" ],
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<h1>Reset Your Password</h1>
2+
3+
<p>Need to reset your password? No Problem! Just click the button below and you'll be on your way. If you did not make this request, please ignore this email.</p>
4+
5+
<table width="100%" cellspacing="0" cellpadding="0">
6+
<tr>
7+
<td>
8+
<table cellspacing="0" cellpadding="0">
9+
<tr>
10+
<td style="border-radius: 2px;" bgcolor="#ED2939">
11+
<a href="{{ emailResetLink }}" 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;">
12+
Reset Your Password
13+
</a>
14+
</td>
15+
</tr>
16+
</table>
17+
</td>
18+
</tr>
19+
</table>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Reset Your Password
2+
3+
Need to reset your password? No Problem! Just click the link below and you'll be on your way. If you did not make this request, please ignore this email.
4+
5+
{{ emailResetLink }}

package-lock.json

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "bmore-responsive",
33
"version": "1.0.0",
44
"description": "An emergency response API",
5-
"main": "/src/app.js",
5+
"main": "src/index.js",
66
"directories": {
77
"doc": "docs"
88
},
@@ -44,7 +44,9 @@
4444
"lodash": "^4.17.15",
4545
"mocha": "7.1.1",
4646
"morgan": "1.9.1",
47+
"nodemailer": "^6.4.6",
4748
"nodemon": "2.0.2",
49+
"nunjucks": "^3.2.1",
4850
"pg": "7.18.2",
4951
"random-words": "1.1.0",
5052
"sequelize": "5.21.5",

src/email/index.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import nodemailer from "nodemailer";
2+
import nunjucks from "nunjucks";
3+
4+
const transporter = nodemailer.createTransport({
5+
host: process.env.SMTP_HOST,
6+
port: process.env.SMTP_PORT,
7+
requireTLS: true,
8+
auth: {
9+
user: process.env.SMTP_USER,
10+
pass: process.env.SMTP_PASSWORD
11+
}
12+
});
13+
14+
/**
15+
* Generic email send function
16+
* @param {string} to address to send mail to
17+
* @param {string} subject email subject
18+
* @param {string} html html text of the email
19+
* @param {string} text plain text of the email
20+
*/
21+
const sendMail = async (to, subject, html, text) => {
22+
let info = await transporter.sendMail({
23+
from: `"Healthcare Roll Call" <${process.env.SMTP_USER}>`, // sender address
24+
to, // list of receivers
25+
subject, // Subject line
26+
text, // plain text body
27+
html // html body
28+
});
29+
console.log("Email sent: %s", info.messageId);
30+
};
31+
32+
/**
33+
* Send a forgot password email.
34+
* @param {string} userEmail email address of the user we're sending to
35+
* @param {string} resetPasswordToken temporary token for the reset password link
36+
*/
37+
const sendForgotPassword = async (userEmail, resetPasswordToken) => {
38+
const emailResetLink = `https://healthcarerollcall.org/reset/${resetPasswordToken}`;
39+
await sendMail(
40+
userEmail,
41+
"Password Reset - Healthcare Roll Call",
42+
nunjucks.render("forgot_password_html.njk", { emailResetLink }),
43+
nunjucks.render("forgot_password_text.njk", { emailResetLink })
44+
);
45+
return true;
46+
};
47+
48+
export default { sendForgotPassword };

src/index.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import requestId from 'express-request-id';
66
import helmet from 'helmet';
77
import morgan from 'morgan';
88
import swaggerUi from 'swagger-ui-express';
9+
import nunjucks from 'nunjucks';
910

1011
import swaggerDocument from '../docs/swagger/swagger.json';
1112
import models, { sequelize } from './models';
@@ -17,6 +18,8 @@ const swaggerOptions = {
1718
customCss: '.swagger-ui .topbar { display: none }'
1819
};
1920

21+
nunjucks.configure('mail_templates', { autoescape: true });
22+
2023
// Third-party middleware
2124
app.use(requestId());
2225
app.use(morgan('tiny'));
@@ -32,11 +35,6 @@ app.use(async (req, res, next) => {
3235
models
3336
};
3437

35-
/** @todo add some checks for auth tokens, etc */
36-
// req.context.me = {
37-
// id: 'abc-123'
38-
// };
39-
4038
next();
4139
});
4240

src/models/user.js

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ const user = (sequelize, DataTypes) => {
2424
salt: {
2525
type: DataTypes.STRING
2626
},
27-
token: {
28-
type: DataTypes.STRING
29-
},
3027
roles: {
3128
type: DataTypes.JSON
3229
},
@@ -56,8 +53,7 @@ const user = (sequelize, DataTypes) => {
5653
if (user) {
5754
const pw = User.encryptPassword(password, user.salt);
5855
if (pw === user.password) {
59-
await user.update();
60-
return user.token;
56+
return await User.getToken(user.id);
6157
}
6258
}
6359
};
@@ -71,27 +67,32 @@ const user = (sequelize, DataTypes) => {
7167
*/
7268
User.validateToken = async token => {
7369
/** @todo check if it is a token at all */
74-
if (!token) return false;
75-
76-
const decoded = jwt.decode(token);
77-
const now = new Date();
78-
79-
if (now.getTime() < decoded.exp * 1000) {
80-
const user = await User.findOne({
81-
where: {token}
82-
});
83-
if (user) {
84-
try {
85-
return await jwt.verify(user.token, process.env.JWT_KEY, {email: user.email});
86-
} catch {
87-
return false;
70+
if (token) {
71+
try {
72+
const decoded = jwt.verify(token, process.env.JWT_KEY);
73+
const now = new Date();
74+
if (now.getTime() < decoded.exp * 1000) {
75+
const user = await User.findByPk(decoded.userId);
76+
if (user) {
77+
return user;
78+
}
8879
}
80+
} catch (e) {
81+
console.error(e);
8982
}
90-
}
91-
83+
}
9284
return false;
9385
};
9486

87+
User.getToken = async (userId, expiresIn = '1d') => {
88+
const token = jwt.sign(
89+
{userId},
90+
process.env.JWT_KEY,
91+
{expiresIn}
92+
);
93+
return token;
94+
};
95+
9596
/**
9697
* Generates a random salt for password security.
9798
*
@@ -126,15 +127,6 @@ const user = (sequelize, DataTypes) => {
126127
}
127128
};
128129

129-
const setToken = user => {
130-
const token = jwt.sign(
131-
{email: user.email},
132-
process.env.JWT_KEY,
133-
{expiresIn: '1d'}
134-
);
135-
user.token = token;
136-
};
137-
138130
// Other Helpers
139131
// const validateContactInfo = user => {
140132
// let valid = true;
@@ -155,7 +147,6 @@ const user = (sequelize, DataTypes) => {
155147
// Update prep actions
156148
// User.beforeUpdate(validateContactInfo);
157149
User.beforeUpdate(setSaltAndPassword);
158-
User.beforeUpdate(setToken);
159150

160151
return User;
161152
};

0 commit comments

Comments
 (0)