Skip to content

Commit d50d75c

Browse files
committed
feat: email verification
1 parent 7994a58 commit d50d75c

File tree

10 files changed

+576
-404
lines changed

10 files changed

+576
-404
lines changed

app/passport.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ passport.use(new GoogleStrategy({
102102
return done(null, user);
103103
} else {
104104
let user = await addUser(newUser);
105-
await sendEmail(newUser.email, `${process.env.SECURED_DOMAIN_WITHOUT_PROTOCOL}> Registration`, "<b>Thank you for registering</b>");
105+
await sendEmail(
106+
newUser.email,
107+
`${process.env.SECURED_DOMAIN_WITHOUT_PROTOCOL}> Registration`,
108+
`<p><b>Thank you for registering</b></p>\
109+
<p><a href="${process.env.SECURED_DOMAIN_WITHOUT_PROTOCOL}/verify-email?email=${encodeURIComponent(newUser.email)}&userID=${user._id}">Verify this email address</a></p>`
110+
);
106111
return done(null, newUser);
107112
}
108113
} catch(error) {

database/methods/users.js

+25-10
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,13 @@ async function findUserByID(id){
7474

7575
async function graphql_findUserByID(_, { userID }){
7676
const db = getDB();
77-
let user = await db.collection('users').findOne({ _id: new mongo.ObjectID(userID) });
77+
let user = await findUserByID(userID);
7878
return user;
7979
}
8080

8181
async function addCompletedTopic(userID, topic){
8282
const db = getDB();
83-
let user = await db.collection('users').findOne({ _id: new mongo.ObjectID(userID) });
83+
let user = await findUserByID(userID);
8484
// Sync completed topics
8585
let completedTopics = user.completedTopics || [];
8686
let topicAlreadyCompleted = false;
@@ -97,7 +97,7 @@ async function addCompletedTopic(userID, topic){
9797

9898
async function removeCompletedTopic(userID, topicID){
9999
const db = getDB();
100-
let user = await db.collection('users').findOne({ _id: new mongo.ObjectID(userID) });
100+
let user = await findUserByID(userID);
101101
// Sync completed topics
102102
let completedTopics = user.completedTopics || [];
103103
let newCompletedTopics = [];
@@ -112,14 +112,14 @@ async function removeCompletedTopic(userID, topicID){
112112
async function verifyUser(_, {userID, verificationStatus}){
113113
const db = getDB();
114114
await db.collection('users').updateOne({ _id: new mongo.ObjectID(userID) }, { $set: { verified: verificationStatus } });
115-
const user = await db.collection('users').findOne({ _id: new mongo.ObjectID(userID) });
115+
const user = await findUserByID(userID);
116116
return user;
117117
}
118118

119119
async function addStripeAccountIDToUser(userID, connectedStripeAccountID){
120120
const db = getDB();
121121
await db.collection('users').updateOne({ _id: new mongo.ObjectID(userID) }, { $set: { connectedStripeAccountID } });
122-
const user = await db.collection('users').findOne({ _id: new mongo.ObjectID(userID) });
122+
const user = await findUserByID(userID);
123123
return user;
124124
}
125125

@@ -144,17 +144,21 @@ async function updateUser(_, { userID, user, profilePictureFile }){
144144
}
145145
delete user.password;
146146

147+
const originalUser = await findUserByID(userID);
148+
const newEmail = user.email !== originalUser.email;
147149

148-
user = await db.collection('users').findOneAndUpdate(
150+
let updatedUser = await db.collection('users').findOneAndUpdate(
149151
{ _id: new mongo.ObjectID(userID) },
150152
{ $set: { ...user } },
151153
{ returnOriginal: false }
152154
);
153-
user = user.value;
154-
await db.collection('users').updateOne({ _id: new mongo.ObjectID(user.userID) }, { $set: { user } });
155-
return user;
155+
updatedUser = updatedUser.value;
156+
157+
if (newEmail) {
158+
updatedUser = await setVerifiedEmail(userID, false);
159+
}
156160

157-
return false;
161+
return updatedUser;
158162
}
159163

160164
async function getRecentUsers(_){
@@ -183,6 +187,16 @@ async function searchUsers(_, { searchQuery }){
183187
return { users };
184188
}
185189

190+
async function setVerifiedEmail(userID, verificationStatus){
191+
const db = getDB();
192+
const updatedUser = await db.collection('users').findOneAndUpdate(
193+
{ _id: new mongo.ObjectID(userID) },
194+
{ $set: { verifiedEmail: verificationStatus } },
195+
{ returnOriginal: false }
196+
);
197+
return updatedUser.value;
198+
}
199+
186200
module.exports = {
187201
addUser,
188202
updateUser,
@@ -195,4 +209,5 @@ module.exports = {
195209
addStripeAccountIDToUser,
196210
getRecentUsers,
197211
searchUsers,
212+
setVerifiedEmail,
198213
};

graphql/schema.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type User {
3030
products: [Product!]
3131
premiumVideoChatListing: PremiumVideoChatListing
3232
connectedStripeAccountID: String
33+
verifiedEmail: Boolean
3334
}
3435

3536
input UserInputs {

js/components/ProfileEditForm.js

+44-4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ var free_solid_svg_icons_1 = require("@fortawesome/free-solid-svg-icons");
6868
var graphQLFetch_js_1 = require("../graphQLFetch.js");
6969
var Navigation_jsx_1 = require("./Navigation.jsx");
7070
var decipher_js_1 = require("../decipher.js");
71+
var emailFetch_js_1 = require("../emailFetch.js");
7172
var ProfileEditForm = /** @class */ (function (_super) {
7273
__extends(ProfileEditForm, _super);
7374
function ProfileEditForm(props) {
@@ -81,11 +82,13 @@ var ProfileEditForm = /** @class */ (function (_super) {
8182
confirmPassword: '',
8283
profilePictureSrc: '',
8384
savedAllChanges: true,
85+
emailVerificationSent: false,
8486
};
8587
_this.state = state;
8688
_this.handleProfilePictureChange = _this.handleProfilePictureChange.bind(_this);
8789
_this.handleSubmit = _this.handleSubmit.bind(_this);
8890
_this.handleDeleteUser = _this.handleDeleteUser.bind(_this);
91+
_this.handleSendEmailVerification = _this.handleSendEmailVerification.bind(_this);
8992
return _this;
9093
}
9194
ProfileEditForm.prototype.componentDidMount = function () {
@@ -171,7 +174,7 @@ var ProfileEditForm = /** @class */ (function (_super) {
171174
mutationName = void 0;
172175
if (!savedUser) return [3 /*break*/, 2];
173176
// If updating existing
174-
query = "mutation updateUser($userID: ID!, $user: UserInputs, $file: Upload){\n updateUser(userID: $userID, user: $user, profilePictureFile: $file){\n _id\n email\n displayName\n firstName\n lastName\n profilePictureSrc\n }\n }";
177+
query = "mutation updateUser($userID: ID!, $user: UserInputs, $file: Upload){\n updateUser(userID: $userID, user: $user, profilePictureFile: $file){\n _id\n email\n displayName\n firstName\n lastName\n profilePictureSrc\n verifiedEmail\n }\n }";
175178
variables = {
176179
userID: savedUser._id,
177180
user: {
@@ -235,14 +238,51 @@ var ProfileEditForm = /** @class */ (function (_super) {
235238
});
236239
});
237240
};
241+
ProfileEditForm.prototype.handleSendEmailVerification = function () {
242+
return __awaiter(this, void 0, void 0, function () {
243+
var user, host, host, host, emailResponse;
244+
return __generator(this, function (_a) {
245+
switch (_a.label) {
246+
case 0:
247+
this.setState({ emailVerificationSent: true });
248+
user = this.state.savedUser;
249+
try {
250+
host = "https://".concat(process.env.SECURED_DOMAIN_WITHOUT_PROTOCOL);
251+
}
252+
catch (e) {
253+
try {
254+
host = "https://localhost:".concat(process.env.APP_PORT);
255+
}
256+
catch (e) {
257+
host = 'https://localhost:3000';
258+
}
259+
}
260+
return [4 /*yield*/, (0, emailFetch_js_1.sendEmailToUser)(user._id, 'Email Verification', "<p><a href=\"".concat(host, "/verify-email?email=").concat(encodeURIComponent(user.email), "&userID=").concat(user._id, "\">Verify this email address</a></p>"))];
261+
case 1:
262+
emailResponse = _a.sent();
263+
return [2 /*return*/];
264+
}
265+
});
266+
});
267+
};
238268
ProfileEditForm.prototype.render = function () {
239269
var _this = this;
240-
var _a = this.state, email = _a.email, displayName = _a.displayName, firstName = _a.firstName, lastName = _a.lastName, password = _a.password, confirmPassword = _a.confirmPassword, profilePictureSrc = _a.profilePictureSrc, savedUser = _a.savedUser;
270+
var _a = this.state, email = _a.email, displayName = _a.displayName, firstName = _a.firstName, lastName = _a.lastName, password = _a.password, confirmPassword = _a.confirmPassword, profilePictureSrc = _a.profilePictureSrc, savedUser = _a.savedUser, savedAllChanges = _a.savedAllChanges, emailVerificationSent = _a.emailVerificationSent;
271+
var verifiedEmail = false;
272+
if (savedUser) {
273+
verifiedEmail = savedUser.verifiedEmail;
274+
}
241275
return (React.createElement("div", { className: "frame fw-container fw-typography-spacing" },
242276
React.createElement(Navigation_jsx_1.default, null),
243277
React.createElement("section", { className: "fw-space double noleft noright" },
244278
React.createElement("div", { className: "pure-g" },
245279
React.createElement("h2", { className: "pure-u-1" }, "User Profile"),
280+
!verifiedEmail && savedAllChanges ?
281+
React.createElement("button", { className: "button", onClick: this.handleSendEmailVerification },
282+
emailVerificationSent ? 'Email Verification Sent' : 'Verify this email',
283+
React.createElement(react_fontawesome_1.FontAwesomeIcon, { icon: free_solid_svg_icons_1.faEnvelope }))
284+
:
285+
'',
246286
React.createElement("form", { className: "pure-u-1 fw-form" },
247287
React.createElement("div", { className: "flex-container desktop-100" },
248288
React.createElement("div", { className: "desktop-50 phone-100" },
@@ -270,8 +310,8 @@ var ProfileEditForm = /** @class */ (function (_super) {
270310
"Profile Picture",
271311
React.createElement(react_fontawesome_1.FontAwesomeIcon, { icon: free_solid_svg_icons_1.faUpload }))),
272312
React.createElement("div", null,
273-
React.createElement("button", { className: "button", onClick: this.handleSubmit, disabled: this.state.savedAllChanges },
274-
this.state.savedAllChanges ? 'Saved' : 'Save',
313+
React.createElement("button", { className: "button", onClick: this.handleSubmit, disabled: savedAllChanges },
314+
savedAllChanges ? 'Saved' : 'Save',
275315
React.createElement(react_fontawesome_1.FontAwesomeIcon, { icon: free_solid_svg_icons_1.faCheck })))),
276316
React.createElement("div", { className: "desktop-30 phone-100" },
277317
React.createElement("div", { className: "desktop-100", style: { maxWidth: '100%', height: '300px' } },

js/components/ProfileEditForm.tsx

+57-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as React from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import { faUpload, faCheck, faTrash, faPlus, faTimes } from '@fortawesome/free-solid-svg-icons';
3+
import { faUpload, faCheck, faTrash, faEnvelope } from '@fortawesome/free-solid-svg-icons';
44
import graphQLFetch from '../graphQLFetch.js';
55
import RemoveConfirmationModal from './RemoveConfirmationModal.js';
66
import Navigation from './Navigation.jsx';
77
import decipher from '../decipher.js';
8+
import { sendEmailToUser } from '../emailFetch.js';
89

910
interface User {
1011
_id: string;
@@ -13,6 +14,7 @@ interface User {
1314
firstName: string
1415
lastName: string
1516
password: string
17+
verifiedEmail: boolean
1618
}
1719

1820
interface ProfileEditState {
@@ -27,6 +29,7 @@ interface ProfileEditState {
2729
savedUser?: User
2830
savedAllChanges: boolean
2931
userID?: string
32+
emailVerificationSent: boolean
3033
}
3134

3235
interface ProfileEditProps {
@@ -47,11 +50,13 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
4750
confirmPassword: '',
4851
profilePictureSrc: '',
4952
savedAllChanges: true,
53+
emailVerificationSent: false,
5054
}
5155
this.state = state;
5256
this.handleProfilePictureChange = this.handleProfilePictureChange.bind(this);
5357
this.handleSubmit = this.handleSubmit.bind(this);
5458
this.handleDeleteUser = this.handleDeleteUser.bind(this);
59+
this.handleSendEmailVerification = this.handleSendEmailVerification.bind(this);
5560
}
5661

5762
async componentDidMount(){
@@ -108,7 +113,7 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
108113
}
109114
}
110115

111-
async handleSubmit(event){
116+
async handleSubmit(event) {
112117

113118
event.preventDefault();
114119

@@ -128,11 +133,11 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
128133
return alert('Passwords do not match.');
129134
}
130135

131-
if(email && displayName && firstName){
136+
if (email && displayName && firstName) {
132137
let query;
133138
let variables;
134139
let mutationName;
135-
if(savedUser){
140+
if (savedUser) {
136141
// If updating existing
137142
query = `mutation updateUser($userID: ID!, $user: UserInputs, $file: Upload){
138143
updateUser(userID: $userID, user: $user, profilePictureFile: $file){
@@ -142,6 +147,7 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
142147
firstName
143148
lastName
144149
profilePictureSrc
150+
verifiedEmail
145151
}
146152
}`;
147153
variables = {
@@ -208,6 +214,27 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
208214
});
209215
}
210216

217+
async handleSendEmailVerification() {
218+
219+
this.setState({ emailVerificationSent: true });
220+
221+
const user = this.state.savedUser;
222+
try {
223+
var host = `https://${process.env.SECURED_DOMAIN_WITHOUT_PROTOCOL}`;
224+
} catch(e) {
225+
try {
226+
var host = `https://localhost:${process.env.APP_PORT}`;
227+
} catch(e) {
228+
var host = 'https://localhost:3000';
229+
}
230+
}
231+
const emailResponse = await sendEmailToUser(
232+
user._id,
233+
'Email Verification',
234+
`<p><a href="${host}/verify-email?email=${encodeURIComponent(user.email)}&userID=${user._id}">Verify this email address</a></p>`
235+
);
236+
}
237+
211238
render(){
212239

213240
let {
@@ -218,9 +245,16 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
218245
password,
219246
confirmPassword,
220247
profilePictureSrc,
221-
savedUser
248+
savedUser,
249+
savedAllChanges,
250+
emailVerificationSent,
222251
} = this.state;
223252

253+
let verifiedEmail = false;
254+
if (savedUser) {
255+
verifiedEmail = savedUser.verifiedEmail;
256+
}
257+
224258
return(
225259

226260
<div className="frame fw-container fw-typography-spacing">
@@ -230,29 +264,45 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
230264

231265
<div className="pure-g">
232266
<h2 className="pure-u-1">User Profile</h2>
267+
268+
{!verifiedEmail && savedAllChanges ?
269+
<button className="button" onClick={this.handleSendEmailVerification}>
270+
{emailVerificationSent ? 'Email Verification Sent' : 'Verify this email'}
271+
<FontAwesomeIcon icon={faEnvelope}/>
272+
</button>
273+
:
274+
''
275+
}
276+
233277
<form className="pure-u-1 fw-form">
234278
<div className="flex-container desktop-100">
235279
<div className="desktop-50 phone-100">
280+
236281
<div className="field text">
237282
<label htmlFor="emailField">Email</label>
238283
<input type="email" name="email" id="emailField" value={email} onChange={(event) => this.setState({email: event.target.value, savedAllChanges: false})}/>
239284
</div>
285+
240286
<div className="field text">
241287
<label htmlFor="displayNameField">Display Name</label>
242288
<input type="text" name="displayName" id="displayNameField" value={displayName} onChange={(event) => this.setState({displayName: event.target.value, savedAllChanges: false})}/>
243289
</div>
290+
244291
<div className="field text">
245292
<label htmlFor="firstNameField">First Name</label>
246293
<input type="text" name="firstName" id="firstNameField" value={firstName} onChange={(event) => this.setState({firstName: event.target.value, savedAllChanges: false})}/>
247294
</div>
295+
248296
<div className="field text">
249297
<label htmlFor="lastNameField">Last Name</label>
250298
<input type="text" name="lastName" id="lastNameField" value={lastName} onChange={(event) => this.setState({lastName: event.target.value, savedAllChanges: false})}/>
251299
</div>
300+
252301
<div className="field text">
253302
<label htmlFor="passwordField">Password</label>
254303
<input type="password" name="password" id="passwordField" value={password} onChange={(event) => this.setState({password: event.target.value, savedAllChanges: false})}/>
255304
</div>
305+
256306
<div className="field text">
257307
<label htmlFor="confirmPasswordField">Confirm password</label>
258308
<input type="password" name="confirmPassword" id="confirmPasswordField" value={confirmPassword} onChange={(event) => this.setState({confirmPassword: event.target.value, savedAllChanges: false})}/>
@@ -267,8 +317,8 @@ export default class ProfileEditForm extends React.Component<ProfileEditProps, P
267317
</div>
268318

269319
<div>
270-
<button className="button" onClick={this.handleSubmit} disabled={this.state.savedAllChanges}>
271-
{this.state.savedAllChanges ? 'Saved' : 'Save'}
320+
<button className="button" onClick={this.handleSubmit} disabled={savedAllChanges}>
321+
{savedAllChanges ? 'Saved' : 'Save'}
272322
<FontAwesomeIcon icon={faCheck}/>
273323
</button>
274324
</div>

0 commit comments

Comments
 (0)