Skip to content

Commit ea38574

Browse files
committed
added schema validation with @hapi/joi and swagger api docs
1 parent 0bc4655 commit ea38574

11 files changed

+924
-83
lines changed

_helpers/authorize.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const expressJwt = require('express-jwt');
22
const { secret } = require('config.json');
3-
const accountService = require('accounts/account.service');
3+
const db = require('./db');
44

55
module.exports = authorize;
66

@@ -17,8 +17,8 @@ function authorize(roles = []) {
1717

1818
// authorize based on user role
1919
async (req, res, next) => {
20-
const account = await accountService.getById(req.user.id);
21-
20+
const account = await db.Account.findById(req.user.id);
21+
2222
if (!account || (roles.length && !roles.includes(account.role))) {
2323
// account no longer exists or role not authorized
2424
return res.status(401).json({ message: 'Unauthorized' });

_helpers/db.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ mongoose.connect(process.env.MONGODB_URI || config.connectionString, connectionO
55
mongoose.Promise = global.Promise;
66

77
module.exports = {
8-
Account: require('../accounts/account.model')
9-
};
8+
Account: require('../accounts/account.model'),
9+
isValidId
10+
};
11+
12+
function isValidId(id) {
13+
return mongoose.Types.ObjectId.isValid(id);
14+
}

_helpers/error-handler.js

+14-16
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
module.exports = errorHandler;
22

33
function errorHandler(err, req, res, next) {
4-
if (typeof (err) === 'string') {
5-
// custom application error
6-
return res.status(400).json({ message: err });
4+
switch (true) {
5+
case typeof err === 'string':
6+
// custom application error
7+
const is404 = err.toLowerCase().endsWith('not found');
8+
const statusCode = is404 ? 404 : 400;
9+
return res.status(statusCode).json({ message: err });
10+
case err.name === 'ValidationError':
11+
// mongoose validation error
12+
return res.status(400).json({ message: err.message });
13+
case err.name === 'UnauthorizedError':
14+
// jwt authentication error
15+
return res.status(401).json({ message: 'Invalid Token' });
16+
default:
17+
return res.status(500).json({ message: err.message });
718
}
8-
9-
if (err.name === 'ValidationError') {
10-
// mongoose validation error
11-
return res.status(400).json({ message: err.message });
12-
}
13-
14-
if (err.name === 'UnauthorizedError') {
15-
// jwt authentication error
16-
return res.status(401).json({ message: 'Invalid Token' });
17-
}
18-
19-
// default to 500 server error
20-
return res.status(500).json({ message: err.message });
2119
}

_helpers/swagger.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const swaggerUi = require('swagger-ui-express');
4+
const YAML = require('yamljs');
5+
const swaggerDocument = YAML.load('./swagger.yaml');
6+
7+
router.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
8+
9+
module.exports = router;

_helpers/validate-request.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = validateRequest;
2+
3+
function validateRequest(req, next, schema) {
4+
const options = {
5+
abortEarly: false, // include all errors
6+
allowUnknown: true, // ignore unknown props
7+
stripUnknown: true // remove unknown props
8+
};
9+
const { error, value } = schema.validate(req.body, options);
10+
if (error) {
11+
next(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
12+
} else {
13+
req.body = value;
14+
next();
15+
}
16+
}

accounts/account.service.js

+89-37
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,20 @@ module.exports = {
2424
async function authenticate({ email, password }) {
2525
const account = await Account.findOne({ email, isVerified: true });
2626
if (account && bcrypt.compareSync(password, account.passwordHash)) {
27+
// return basic details and auth token
2728
const token = jwt.sign({ sub: account.id, id: account.id }, config.secret);
28-
return {
29-
...account.toJSON(),
30-
token
31-
};
29+
return { ...basicDetails(account), token };
3230
}
3331
}
3432

3533
async function register(params, origin) {
3634
// validate
3735
if (await Account.findOne({ email: params.email })) {
38-
// send already registered notification in email to prevent account enumeration
39-
return sendEmail({
40-
to: params.email,
41-
subject: 'Sign-up Verification API - Email Already Registered',
42-
html: `<h4>Email Already Registered</h4>
43-
<p>Your email <strong>${params.email}</strong> is already registered.</p>
44-
<p>If you don't know your password please visit the <a href="${origin}/account/forgot-password">forgot password</a> page.</p>`
45-
});
36+
// send already registered error in email to prevent account enumeration
37+
return sendAlreadyRegisteredEmail(params.email, origin);
4638
}
4739

40+
// create account object
4841
const account = new Account(params);
4942

5043
// first registered account is an admin
@@ -61,16 +54,8 @@ async function register(params, origin) {
6154
// save account
6255
await account.save();
6356

64-
// send verification email
65-
const verifyUrl = `${origin}/account/verify-email?token=${account.verificationToken}`;
66-
return sendEmail({
67-
to: params.email,
68-
subject: 'Sign-up Verification API - Verify Email',
69-
html: `<h4>Verify Email</h4>
70-
<p>Thanks for registering!</p>
71-
<p>Please click the below link to verify your email address:</p>
72-
<p><a href="${verifyUrl}">${verifyUrl}</a></p>`
73-
});
57+
// send email
58+
sendVerificationEmail(account, origin);
7459
}
7560

7661
async function verifyEmail({ token }) {
@@ -93,15 +78,8 @@ async function forgotPassword({ email }, origin) {
9378
account.resetTokenExpiry = new Date(Date.now() + 24*60*60*1000).toISOString();
9479
account.save();
9580

96-
// send password reset email
97-
const resetUrl = `${origin}/account/reset-password?token=${account.resetToken}`;
98-
sendEmail({
99-
to: email,
100-
subject: 'Sign-up Verification API - Reset Password',
101-
html: `<h4>Reset Password Email</h4>
102-
<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
103-
<p><a href="${resetUrl}">${resetUrl}</a></p>`
104-
})
81+
// send email
82+
sendPasswordResetEmail(account, origin);
10583
}
10684

10785
async function validateResetToken({ token }) {
@@ -130,11 +108,13 @@ async function resetPassword({ token, password }) {
130108
}
131109

132110
async function getAll() {
133-
return await Account.find();
111+
const accounts = await Account.find();
112+
return accounts.map(x => basicDetails(x));
134113
}
135114

136115
async function getById(id) {
137-
return await Account.findById(id);
116+
const account = await getAccount(id);
117+
return basicDetails(account);
138118
}
139119

140120
async function create(params) {
@@ -153,13 +133,14 @@ async function create(params) {
153133

154134
// save account
155135
await account.save();
136+
137+
return basicDetails(account);
156138
}
157139

158140
async function update(id, params) {
159-
const account = await Account.findById(id);
141+
const account = await getAccount(id);
160142

161143
// validate
162-
if (!account) throw 'Account not found';
163144
if (account.email !== params.email && await Account.findOne({ email: params.email })) {
164145
throw 'Email "' + params.email + '" is already taken';
165146
}
@@ -171,20 +152,91 @@ async function update(id, params) {
171152

172153
// copy params to account and save
173154
Object.assign(account, params);
155+
account.dateUpdated = Date.now();
174156
await account.save();
175-
return account.toJSON();
157+
158+
return basicDetails(account);
176159
}
177160

178161
async function _delete(id) {
179-
await Account.findByIdAndRemove(id);
162+
const account = await getAccount(id);
163+
await account.remove();
180164
}
181165

182166
// helper functions
183167

168+
async function getAccount(id) {
169+
if (!db.isValidId(id)) throw 'Account not found';
170+
const account = await Account.findById(id);
171+
if (!account) throw 'Account not found';
172+
return account;
173+
}
174+
184175
function hash(password) {
185176
return bcrypt.hashSync(password, 10);
186177
}
187178

188179
function generateToken() {
189180
return crypto.randomBytes(40).toString('hex');
181+
}
182+
183+
function basicDetails(account) {
184+
const { id, title, firstName, lastName, email, role, dateCreated, dateUpdated } = account;
185+
return { id, title, firstName, lastName, email, role, dateCreated, dateUpdated };
186+
}
187+
188+
function sendVerificationEmail(account, origin) {
189+
let message;
190+
if (origin) {
191+
const verifyUrl = `${origin}/account/verify-email?token=${account.verificationToken}`;
192+
message = `<p>Please click the below link to verify your email address:</p>
193+
<p><a href="${verifyUrl}">${verifyUrl}</a></p>`;
194+
} else {
195+
message = `<p>Please use the below token to verify your email address with the <code>/account/verify-email</code> api route:</p>
196+
<p><code>${account.verificationToken}</code></p>`;
197+
}
198+
199+
sendEmail({
200+
to: account.email,
201+
subject: 'Sign-up Verification API - Verify Email',
202+
html: `<h4>Verify Email</h4>
203+
<p>Thanks for registering!</p>
204+
${message}`
205+
});
206+
}
207+
208+
function sendAlreadyRegisteredEmail(email, origin) {
209+
let message;
210+
if (origin) {
211+
message = `<p>If you don't know your password please visit the <a href="${origin}/account/forgot-password">forgot password</a> page.</p>`;
212+
} else {
213+
message = `<p>If you don't know your password you can reset it via the <code>/account/forgot-password</code> api route.</p>`;
214+
}
215+
216+
sendEmail({
217+
to: email,
218+
subject: 'Sign-up Verification API - Email Already Registered',
219+
html: `<h4>Email Already Registered</h4>
220+
<p>Your email <strong>${email}</strong> is already registered.</p>
221+
${message}`
222+
});
223+
}
224+
225+
function sendPasswordResetEmail(account, origin) {
226+
let message;
227+
if (origin) {
228+
const resetUrl = `${origin}/account/reset-password?token=${account.resetToken}`;
229+
message = `<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
230+
<p><a href="${resetUrl}">${resetUrl}</a></p>`;
231+
} else {
232+
message = `<p>Please use the below token to reset your password with the <code>/account/reset-password</code> api route:</p>
233+
<p><code>${account.resetToken}</code></p>`;
234+
}
235+
236+
sendEmail({
237+
to: account.email,
238+
subject: 'Sign-up Verification API - Reset Password',
239+
html: `<h4>Reset Password Email</h4>
240+
${message}`
241+
});
190242
}

0 commit comments

Comments
 (0)