Skip to content

Commit 571d8b7

Browse files
Account invitation logic
1 parent 95167ef commit 571d8b7

10 files changed

Lines changed: 200 additions & 15 deletions

File tree

assets/email/AccountInvitation.hbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<b>
2+
Create Your Account:
3+
<a href="{{link}}">here</a>
4+
</b>

constants/general.constant.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ EMAIL_SUBJECTS[HACKER_STATUS_CONFIRMED] = `Thanks for confirming your attendance
7373
EMAIL_SUBJECTS[HACKER_STATUS_CANCELLED] = "Sorry to see you go";
7474
EMAIL_SUBJECTS[HACKER_STATUS_CHECKED_IN] = `Welcome to ${HACKATHON_NAME}`;
7575

76-
const CONFIRM_ACC_EMAIL_SUBJECTS = {};
77-
CONFIRM_ACC_EMAIL_SUBJECTS[HACKER] = `Please complete your hacker application for ${HACKATHON_NAME}`;
78-
CONFIRM_ACC_EMAIL_SUBJECTS[SPONSOR] = `You've been invited to create a sponsor account for ${HACKATHON_NAME}`;
79-
CONFIRM_ACC_EMAIL_SUBJECTS[VOLUNTEER] = `You've been invited to create a volunteer account for ${HACKATHON_NAME}`;
80-
CONFIRM_ACC_EMAIL_SUBJECTS[STAFF] = `You've been invited to create a staff account for ${HACKATHON_NAME}`;
76+
const CONFIRM_ACC_EMAIL_SUBJECT = `Please complete your hacker application for ${HACKATHON_NAME}`;
77+
const CREATE_ACC_EMAIL_SUBJECTS = {};
78+
CREATE_ACC_EMAIL_SUBJECTS[HACKER] = `You've been invited to create a hacker account for ${HACKATHON_NAME}`;
79+
CREATE_ACC_EMAIL_SUBJECTS[SPONSOR] = `You've been invited to create a sponsor account for ${HACKATHON_NAME}`;
80+
CREATE_ACC_EMAIL_SUBJECTS[VOLUNTEER] = `You've been invited to create a volunteer account for ${HACKATHON_NAME}`;
81+
CREATE_ACC_EMAIL_SUBJECTS[STAFF] = `You've been invited to create a staff account for ${HACKATHON_NAME}`;
8182

8283
module.exports = {
8384
HACKATHON_NAME: HACKATHON_NAME,
@@ -99,7 +100,8 @@ module.exports = {
99100
EXTENDED_USER_TYPES: EXTENDED_USER_TYPES,
100101
URL_REGEX: URL_REGEX,
101102
EMAIL_SUBJECTS: EMAIL_SUBJECTS,
102-
CONFIRM_ACC_EMAIL_SUBJECTS: CONFIRM_ACC_EMAIL_SUBJECTS,
103+
CREATE_ACC_EMAIL_SUBJECTS: CREATE_ACC_EMAIL_SUBJECTS,
104+
CONFIRM_ACC_EMAIL_SUBJECT: CONFIRM_ACC_EMAIL_SUBJECT,
103105
HACKER: HACKER,
104106
SPONSOR: SPONSOR,
105107
VOLUNTEER: VOLUNTEER,

constants/routes.constant.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ const accountRoutes = {
4848
"patchAnyById": {
4949
requestType: Constants.REQUEST_TYPES.PATCH,
5050
uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL,
51+
},
52+
"inviteAccount": {
53+
requestType: Constants.REQUEST_TYPES.POST,
54+
uri: "/api/account/invite"
5155
}
5256
};
5357

controllers/account.controller.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ async function updateAccount(req, res) {
105105
}
106106
}
107107

108+
function invitedAccount(req, res){
109+
return res.status(200).json({
110+
message: "Successfully invited user",
111+
data: {}
112+
})
113+
}
114+
115+
108116
module.exports = {
109117
getUserByEmail: Util.asyncMiddleware(getUserByEmail),
110118
getUserById: Util.asyncMiddleware(getUserById),
@@ -113,4 +121,5 @@ module.exports = {
113121
addUser: Util.asyncMiddleware(addUser),
114122

115123
updateAccount: Util.asyncMiddleware(updateAccount),
124+
invitedAccount: invitedAccount
116125
};

middlewares/account.middleware.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ const mongoose = require("mongoose");
55
const Services = {
66
RoleBinding: require("../services/roleBinding.service"),
77
Logger: require("../services/logger.service"),
8-
Account: require("../services/account.service")
8+
Account: require("../services/account.service"),
9+
AccountConfirmation: require("../services/accountConfirmation.service"),
10+
Email: require("../services/email.service")
911
};
1012

1113
const Middleware = {
@@ -113,12 +115,44 @@ async function addAccount(req, res, next) {
113115
next();
114116
}
115117

118+
/**
119+
* @function inviteAccount
120+
* @param {{body: {email: string, accountType: string}}} req
121+
* @param {*} res
122+
* @param {(err?)=>void} next
123+
* @return {void}
124+
* Creates email to provide a link for the user to create an account
125+
*/
126+
async function inviteAccount(req, res, next) {
127+
const email = req.body.email;
128+
const accountType = req.body.accountType;
129+
const confirmationObj = await Services.AccountConfirmation.create(accountType, email);
130+
const confirmationToken = Services.AccountConfirmation.generateToken(confirmationObj.id);
131+
132+
const mailData = Services.AccountConfirmation.generateAccountInvitationEmail(req.hostname, email, accountType, confirmationToken);
133+
if (mailData !== undefined) {
134+
Services.Email.send(mailData, (err) => {
135+
if (err) {
136+
next(err);
137+
} else {
138+
next();
139+
}
140+
});
141+
} else {
142+
return next({
143+
message: Constants.Error.EMAIL_500_MESSAGE,
144+
});
145+
}
146+
147+
}
148+
116149
module.exports = {
117150
parsePatch: parsePatch,
118151
parseAccount: parseAccount,
119152
// untested
120153
addDefaultHackerPermissions: Middleware.Util.asyncMiddleware(addDefaultHackerPermissions),
121154
// untested
122155
updatePassword: Middleware.Util.asyncMiddleware(updatePassword),
123-
addAccount: Middleware.Util.asyncMiddleware(addAccount)
156+
addAccount: Middleware.Util.asyncMiddleware(addAccount),
157+
inviteAccount: Middleware.Util.asyncMiddleware(inviteAccount)
124158
};

middlewares/validators/account.validator.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@ module.exports = {
2424
VALIDATOR.shirtSizeValidator("body", "shirtSize", true),
2525
VALIDATOR.dateValidator("body", "birthDate", true),
2626
VALIDATOR.phoneNumberValidator("body", "phoneNumber", true)
27+
],
28+
inviteAccountValidator: [
29+
VALIDATOR.emailValidator("body", "email", false),
30+
VALIDATOR.accountTypeValidator("body", "accountType", false)
2731
]
2832
};

middlewares/validators/validator.helper.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,26 @@ function phoneNumberValidator(fieldLocation, fieldname, optional = true) {
552552
}
553553
}
554554

555+
/**
556+
* Validates that field must be a valid account type
557+
* @param {"query" | "body" | "header" | "param"} fieldLocation the location where the field should be found
558+
* @param {string} fieldname name of the field that needs to be validated.
559+
* @param {boolean} optional whether the field is optional or not.
560+
*/
561+
function accountTypeValidator(fieldLocation, fieldname, optional = true) {
562+
const accountType = setProperValidationChainBuilder(fieldLocation, fieldname, "Invalid account type");
563+
if (optional) {
564+
return accountType.optional({
565+
checkFalsy: true
566+
}).isIn(Constants.EXTENDED_USER_TYPES);
567+
}
568+
else{
569+
return accountType.exists()
570+
.withMessage("Account type must be provided")
571+
.isIn(Constants.EXTENDED_USER_TYPES);
572+
}
573+
}
574+
555575
/**
556576
*
557577
* @param {"query" | "body" | "header" | "param"} fieldLocation the location where the field should be found
@@ -601,5 +621,6 @@ module.exports = {
601621
searchValidator: searchValidator,
602622
searchSortValidator: searchSortValidator,
603623
phoneNumberValidator: phoneNumberValidator,
604-
dateValidator: dateValidator
624+
dateValidator: dateValidator,
625+
accountTypeValidator: accountTypeValidator
605626
};

routes/api/account.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,32 @@ module.exports = {
173173
Controllers.Account.getUserById
174174
);
175175

176+
/**
177+
* @api {post} /account/invite invites a user to create an account with the specified accountType
178+
* @apiName inviteAccount
179+
* @apiGroup Account
180+
* @apiVersion 0.0.8
181+
*
182+
* @apiParam (body) {String} [email] email of the account to be created and where to send the link
183+
* @apiParam (body) {String} [accountType] the type of the account which the user can create, for sponsor this should specify tier as well
184+
*
185+
* @apiSuccess {string} message Success message
186+
* @apiSuccess {object} data Account object
187+
* @apiSuccessExample {object} Success-Response:
188+
* {
189+
"message": "Successfully invited user ",
190+
"data": {}
191+
}
192+
*/
193+
accountRouter.route("/invite").post(
194+
Middleware.Auth.ensureAuthenticated(),
195+
Middleware.Auth.ensureAuthorized(),
196+
Middleware.Validator.Account.inviteAccountValidator,
197+
Middleware.parseBody.middleware,
198+
Middleware.Account.inviteAccount,
199+
Controllers.Account.invitedAccount
200+
);
201+
176202
apiRouter.use("/account", accountRouter);
177203
}
178204
};

services/accountConfirmation.service.js

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async function create(type, email, accountId) {
5757
* @param {ObjectId} accountId
5858
* @returns {string} JWT Token containing accountId and accountConfirmationId
5959
*/
60-
function generateToken(accountConfirmationId, accountId) {
60+
function generateToken(accountConfirmationId, accountId = null) {
6161
const token = jwt.sign({
6262
accountConfirmationId: accountConfirmationId,
6363
accountId: accountId
@@ -68,34 +68,48 @@ function generateToken(accountConfirmationId, accountId) {
6868
}
6969

7070
/**
71-
* Generates the link that the user will use to access the page to finish account creation
71+
* Generates the link that the user will use to access the page to begin account creationg
7272
* @param {'http'|'https'} httpOrHttps
7373
* @param {string} domain the domain of the current
7474
* @param {string} type the model that the
7575
* @param {string} token the reset token
7676
* @returns {string} the string, of form: [http|https]://{domain}/{model}/create?token={token}
7777
*/
78-
function generateTokenLink(httpOrHttps, domain, type, token) {
78+
function generateCreateAccountTokenLink(httpOrHttps, domain, type, token) {
7979
const link = `${httpOrHttps}://${domain}/${type}/create?token=${token}`;
8080
return link;
8181
}
8282

8383
/**
84-
* Generates the mailData for the account confirmation Email.
84+
* Generates the link that the user will use to confirm account and proceed with account creationg
85+
* @param {'http'|'https'} httpOrHttps
86+
* @param {string} domain the domain of the current
87+
* @param {string} type the model that the
88+
* @param {string} token the reset token
89+
* @returns {string} the string, of form: [http|https]://{domain}/{model}/create?token={token}
90+
*/
91+
function generateConfirmTokenLink(httpOrHttps, domain, type, token) {
92+
const link = `${httpOrHttps}://${domain}/${type}/confirm?token=${token}`;
93+
return link;
94+
}
95+
/**
96+
* Generates the mailData for the account confirmation Email. This really only applies to
97+
* hackers as all other accounts are intrinsically confirmed via the email they recieve to invite them
8598
* @param {string} hostname The hostname that this service is running on
8699
* @param {string} receiverEmail The receiver of the email
100+
* @param {string} type the user type
87101
* @param {string} token The account confirmation token
88102
*/
89103
function generateAccountConfirmationEmail(hostname, receiverEmail, type, token) {
90104
const httpOrHttps = (hostname === "localhost") ? "http" : "https";
91105
const address = (hostname === "localhost") ? `localhost:${process.env.PORT}` : hostname;
92-
const tokenLink = generateTokenLink(httpOrHttps, address, type, token);
106+
const tokenLink = generateConfirmTokenLink(httpOrHttps, address, type, token);
93107
var emailSubject = "";
94108
if (token === undefined || tokenLink === undefined) {
95109
return undefined;
96110
}
97111
if (type === Constants.HACKER) {
98-
emailSubject = Constants.CONFIRM_ACC_EMAIL_SUBJECTS[Constants.HACKER];
112+
emailSubject = Constants.CONFIRM_ACC_EMAIL_SUBJECT
99113
}
100114
const handlebarPath = path.join(__dirname, `../assets/email/AccountConfirmation.hbs`);
101115

@@ -109,10 +123,50 @@ function generateAccountConfirmationEmail(hostname, receiverEmail, type, token)
109123
};
110124
return mailData;
111125
}
126+
127+
/*
128+
* Generates the mailData for the account invitation Email.
129+
* @param {string} hostname The hostname that this service is running on
130+
* @param {string} receiverEmail The receiver of the email
131+
* @param {string} type The user type
132+
* @param {string} token The account confirmation token
133+
*/
134+
function generateAccountInvitationEmail(hostname, receiverEmail, type, token) {
135+
const httpOrHttps = (hostname === "localhost") ? "http" : "https";
136+
const address = (hostname === "localhost") ? `localhost:${process.env.PORT}` : hostname;
137+
const tokenLink = generateCreateAccountTokenLink(httpOrHttps, address, type, token);
138+
var emailSubject = "";
139+
if (token === undefined || tokenLink === undefined) {
140+
return undefined;
141+
}
142+
if (type === Constants.HACKER) {
143+
emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.HACKER];
144+
}
145+
else if(type === Constants.VOLUNTEER){
146+
emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.VOLUNTEER];
147+
}
148+
else if(Constants.SPONSOR_TIERS.includes(type)){
149+
emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.SPONSOR];
150+
}
151+
else if(type === Constants.STAFF){
152+
emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.STAFF];
153+
}
154+
const handlebarPath = path.join(__dirname, `../assets/email/AccountInvitation.hbs`);
155+
const mailData = {
156+
from: process.env.NO_REPLY_EMAIL,
157+
to: receiverEmail,
158+
subject: emailSubject,
159+
html: Services.Email.renderEmail(handlebarPath, {
160+
link: tokenLink
161+
})
162+
};
163+
return mailData;
164+
}
112165
module.exports = {
113166
findById: findById,
114167
findByAccountId: findByAccountId,
115168
create: create,
116169
generateToken: generateToken,
117170
generateAccountConfirmationEmail: generateAccountConfirmationEmail,
171+
generateAccountInvitationEmail: generateAccountInvitationEmail
118172
}

tests/account.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const Account = require("../models/account.model");
88
const should = chai.should();
99
const Constants = {
1010
Error: require("../constants/error.constant"),
11+
General: require("../constants/general.constant")
1112
};
1213

1314

@@ -430,4 +431,30 @@ describe("GET resend confirmation email", function () {
430431
})
431432
})
432433
})
434+
});
435+
436+
describe("POST invite account", function () {
437+
it("Should succeed to invite a user to create an account", function(done){
438+
util.auth.login(agent, Admin1, (error) => {
439+
if (error) {
440+
agent.close();
441+
return done(error);
442+
}
443+
return agent
444+
.post("/api/account/invite")
445+
.type("application/json")
446+
.send({email: newAccount1.email,
447+
accountType: Constants.General.VOLUNTEER})
448+
// does not have password because of to stripped json
449+
.end(function (err, res) {
450+
if (err) {
451+
return done(err);
452+
}
453+
res.should.have.status(200);
454+
res.body.should.have.property("message");
455+
res.body.message.should.equal("Successfully invited user");
456+
done();
457+
});
458+
});
459+
})
433460
});

0 commit comments

Comments
 (0)