1
+ const config = require ( 'config.json' ) ;
2
+ const jwt = require ( 'jsonwebtoken' ) ;
3
+ const bcrypt = require ( 'bcryptjs' ) ;
4
+ const crypto = require ( "crypto" ) ;
5
+ const sendEmail = require ( '_helpers/send-email' ) ;
6
+ const db = require ( '_helpers/db' ) ;
7
+ const Role = require ( '_helpers/role' ) ;
8
+ const Account = db . Account ;
9
+
10
+ module . exports = {
11
+ authenticate,
12
+ register,
13
+ verifyEmail,
14
+ forgotPassword,
15
+ validateResetToken,
16
+ resetPassword,
17
+ getAll,
18
+ getById,
19
+ create,
20
+ update,
21
+ delete : _delete
22
+ } ;
23
+
24
+ async function authenticate ( { email, password } ) {
25
+ const account = await Account . findOne ( { email, isVerified : true } ) ;
26
+ if ( account && bcrypt . compareSync ( password , account . passwordHash ) ) {
27
+ const token = jwt . sign ( { sub : account . id , id : account . id } , config . secret ) ;
28
+ return {
29
+ ...account . toJSON ( ) ,
30
+ token
31
+ } ;
32
+ }
33
+ }
34
+
35
+ async function register ( params , origin ) {
36
+ // validate
37
+ 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
+ } ) ;
46
+ }
47
+
48
+ const account = new Account ( params ) ;
49
+
50
+ // first registered account is an admin
51
+ const isFirstAccount = ( await Account . countDocuments ( { } ) ) === 0 ;
52
+ account . role = isFirstAccount ? Role . Admin : Role . User ;
53
+ account . verificationToken = generateToken ( ) ;
54
+ account . isVerified = false ;
55
+
56
+ // hash password
57
+ if ( params . password ) {
58
+ account . passwordHash = hash ( params . password ) ;
59
+ }
60
+
61
+ // save account
62
+ await account . save ( ) ;
63
+
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
+ } ) ;
74
+ }
75
+
76
+ async function verifyEmail ( { token } ) {
77
+ const account = await Account . findOne ( { verificationToken : token } ) ;
78
+
79
+ if ( ! account ) throw 'Verification failed' ;
80
+
81
+ account . isVerified = true ;
82
+ await account . save ( ) ;
83
+ }
84
+
85
+ async function forgotPassword ( { email } , origin ) {
86
+ const account = await Account . findOne ( { email } ) ;
87
+
88
+ // always return ok response to prevent email enumeration
89
+ if ( ! account ) return ;
90
+
91
+ // create reset token that expires after 24 hours
92
+ account . resetToken = generateToken ( ) ;
93
+ account . resetTokenExpiry = new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
94
+ account . save ( ) ;
95
+
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
+ } )
105
+ }
106
+
107
+ async function validateResetToken ( { token } ) {
108
+ const account = await Account . findOne ( {
109
+ resetToken : token ,
110
+ resetTokenExpiry : { $gt : new Date ( ) }
111
+ } ) ;
112
+
113
+ if ( ! account ) throw 'Invalid token' ;
114
+ }
115
+
116
+ async function resetPassword ( { token, password } ) {
117
+ const account = await Account . findOne ( {
118
+ resetToken : token ,
119
+ resetTokenExpiry : { $gt : new Date ( ) }
120
+ } ) ;
121
+
122
+ if ( ! account ) throw 'Invalid token' ;
123
+
124
+ // update password and remove reset token
125
+ account . passwordHash = hash ( password ) ;
126
+ account . isVerified = true ;
127
+ account . resetToken = undefined ;
128
+ account . resetTokenExpiry = undefined ;
129
+ await account . save ( ) ;
130
+ }
131
+
132
+ async function getAll ( ) {
133
+ return await Account . find ( ) ;
134
+ }
135
+
136
+ async function getById ( id ) {
137
+ return await Account . findById ( id ) ;
138
+ }
139
+
140
+ async function create ( params ) {
141
+ // validate
142
+ if ( await Account . findOne ( { email : params . email } ) ) {
143
+ throw 'Email "' + params . email + '" is already registered' ;
144
+ }
145
+
146
+ const account = new Account ( params ) ;
147
+ account . isVerified = true ;
148
+
149
+ // hash password
150
+ if ( params . password ) {
151
+ account . passwordHash = hash ( params . password ) ;
152
+ }
153
+
154
+ // save account
155
+ await account . save ( ) ;
156
+ }
157
+
158
+ async function update ( id , params ) {
159
+ const account = await Account . findById ( id ) ;
160
+
161
+ // validate
162
+ if ( ! account ) throw 'Account not found' ;
163
+ if ( account . email !== params . email && await Account . findOne ( { email : params . email } ) ) {
164
+ throw 'Email "' + params . email + '" is already taken' ;
165
+ }
166
+
167
+ // hash password if it was entered
168
+ if ( params . password ) {
169
+ params . passwordHash = hash ( params . password ) ;
170
+ }
171
+
172
+ // copy params to account and save
173
+ Object . assign ( account , params ) ;
174
+ await account . save ( ) ;
175
+ return account . toJSON ( ) ;
176
+ }
177
+
178
+ async function _delete ( id ) {
179
+ await Account . findByIdAndRemove ( id ) ;
180
+ }
181
+
182
+ // helper functions
183
+
184
+ function hash ( password ) {
185
+ return bcrypt . hashSync ( password , 10 ) ;
186
+ }
187
+
188
+ function generateToken ( ) {
189
+ return crypto . randomBytes ( 40 ) . toString ( 'hex' ) ;
190
+ }
0 commit comments