1
1
import {
2
- bad ,
2
+ emailAlreadyVerified ,
3
3
expiredVerificationCode ,
4
4
forbidden ,
5
5
invalidVerificationCode ,
6
+ notfound ,
6
7
unresolvedPhone ,
7
8
} from "@/lib/error" ;
8
9
import { confirmationCodes , knex , users } from "@litespace/models" ;
9
10
import { IConfirmationCode , IUser } from "@litespace/types" ;
10
11
import {
11
- CONFIRMATION_CODE_VALIDITY_MINUTES ,
12
12
isUser ,
13
+ CONFIRMATION_CODE_VALIDITY_MINUTES ,
13
14
NOTIFICATION_METHOD_LITERAL_TO_KAFKA_TOPIC ,
14
- NOTIFICATION_METHOD_LITERAL_TO_NOTIFICATION_METHOD ,
15
+ NOTIFICATION_METHOD_LITERAL_TO_ENUM ,
15
16
NOTIFICATION_METHOD_LITERAL_TO_PURPOSE ,
16
17
} from "@litespace/utils" ;
17
18
import { NextFunction , Request , Response } from "express" ;
18
19
import zod from "zod" ;
19
20
import dayjs from "@/lib/dayjs" ;
20
21
import safeRequest from "express-async-handler" ;
21
22
import { first } from "lodash" ;
22
- import { generateConfirmationCode } from "@/lib/confirmationCodes" ;
23
+ import { generateConfirmationCode , selectPhone } from "@/lib/confirmationCodes" ;
23
24
import { messenger } from "@/lib/messenger" ;
24
- import { withPhone } from "@/lib/user" ;
25
25
import { producer } from "@/lib/kafka" ;
26
- import { unionOfLiterals } from "@/validation/utils" ;
26
+ import { id , unionOfLiterals , email } from "@/validation/utils" ;
27
+ import { sendBackgroundMessage } from "@/workers" ;
28
+ import { jwtSecret } from "@/constants" ;
29
+ import { encodeAuthJwt } from "@litespace/auth" ;
27
30
28
31
const method = unionOfLiterals < IUser . NotificationMethodLiteral > ( [
29
32
"whatsapp" ,
@@ -40,6 +43,17 @@ const verifyNotificationMethodCodePayload = zod.object({
40
43
method,
41
44
} ) ;
42
45
46
+ const sendCodePayload = zod . object ( { email } ) ;
47
+
48
+ const confirmPasswordCodePayload = zod . object ( {
49
+ userId : id ,
50
+ code : zod . number ( ) ,
51
+ } ) ;
52
+
53
+ const verifyEmailPayload = zod . object ( {
54
+ code : zod . number ( ) ,
55
+ } ) ;
56
+
43
57
async function sendVerifyPhoneCode (
44
58
req : Request ,
45
59
res : Response ,
@@ -51,21 +65,22 @@ async function sendVerifyPhoneCode(
51
65
52
66
const payload : IConfirmationCode . SendVerifyPhoneCodePayload =
53
67
sendVerifyNotificationMethodCodePayload . parse ( req . body ) ;
54
- const { valid, update, phone } = withPhone ( user . phone , payload . phone ) ;
55
68
56
- if ( ! valid || ! phone ) return next ( bad ( "Invalid or missing phone number" ) ) ;
57
- if ( update ) await users . update ( user . id , { phone } ) ;
69
+ const phone = selectPhone ( user . phone , payload . phone ) ;
70
+ if ( phone instanceof Error ) return next ( phone ) ;
71
+ if ( ! user . phone ) await users . update ( user . id , { phone } ) ;
58
72
59
73
const purpose = NOTIFICATION_METHOD_LITERAL_TO_PURPOSE [ payload . method ] ;
60
74
61
- // Remove any confirmation code that blongs to the current user under the same
75
+ // Remove any confirmation code that belongs to the current user under the same
62
76
// purpose. This way users will only have one code under a given purpose at
63
- // any given point of time.
77
+ // any point of time.
64
78
await confirmationCodes . delete ( {
65
79
users : [ user . id ] ,
66
80
purposes : [ purpose ] ,
67
81
} ) ;
68
82
83
+ // Store the new code in the db
69
84
const code = await confirmationCodes . create ( {
70
85
userId : user . id ,
71
86
purpose,
@@ -76,13 +91,14 @@ async function sendVerifyPhoneCode(
76
91
. toISOString ( ) ,
77
92
} ) ;
78
93
94
+ // If the method is telegram; we need to resolve the number first with telegram Api
79
95
const resolvedPhone =
80
96
payload . method !== "telegram" ||
81
97
( await messenger . telegram . resolvePhone ( { phone } ) . then ( ( phone ) => ! ! phone ) ) ;
82
98
if ( ! resolvedPhone ) return next ( unresolvedPhone ( ) ) ;
83
99
100
+ // Send the notification using Kafka Producer
84
101
const topic = NOTIFICATION_METHOD_LITERAL_TO_KAFKA_TOPIC [ payload . method ] ;
85
-
86
102
await producer . send ( {
87
103
topic,
88
104
messages : [
@@ -136,8 +152,7 @@ async function verifyPhoneCode(
136
152
user . id ,
137
153
{
138
154
verifiedPhone : true ,
139
- notificationMethod :
140
- NOTIFICATION_METHOD_LITERAL_TO_NOTIFICATION_METHOD [ method ] ,
155
+ notificationMethod : NOTIFICATION_METHOD_LITERAL_TO_ENUM [ method ] ,
141
156
verifiedWhatsApp,
142
157
verifiedTelegram,
143
158
} ,
@@ -148,7 +163,175 @@ async function verifyPhoneCode(
148
163
res . sendStatus ( 200 ) ;
149
164
}
150
165
166
+ /**
167
+ * This handler generates a random code, sets it in the database
168
+ * and sends it to the user by mail.
169
+ */
170
+ async function sendForgottenPasswordCode (
171
+ req : Request ,
172
+ res : Response ,
173
+ next : NextFunction
174
+ ) {
175
+ const { email } : IConfirmationCode . SendCodeEmailPayload =
176
+ sendCodePayload . parse ( req . body ) ;
177
+
178
+ const user = await users . findByEmail ( email ) ;
179
+ if ( ! user ) return next ( notfound . user ( ) ) ;
180
+
181
+ // Remove any confirmation code that belongs to the current user
182
+ // under the same purpose
183
+ await confirmationCodes . delete ( {
184
+ users : [ user . id ] ,
185
+ purposes : [ IConfirmationCode . Purpose . ResetPassword ] ,
186
+ } ) ;
187
+
188
+ // Generate and store the new code in the db
189
+ const { code } = await confirmationCodes . create ( {
190
+ userId : user . id ,
191
+ purpose : IConfirmationCode . Purpose . ResetPassword ,
192
+ code : generateConfirmationCode ( ) ,
193
+ expiresAt : dayjs
194
+ . utc ( )
195
+ . add ( CONFIRMATION_CODE_VALIDITY_MINUTES , "minutes" )
196
+ . toISOString ( ) ,
197
+ } ) ;
198
+
199
+ if ( user ) {
200
+ sendBackgroundMessage ( {
201
+ type : "send-forget-password-code-email" ,
202
+ payload : { email : user . email , code } ,
203
+ } ) ;
204
+ }
205
+
206
+ res . status ( 200 ) . send ( ) ;
207
+ }
208
+
209
+ /**
210
+ * This handler gets a userId and code from users, verify the validity of the code
211
+ * and generates, then send, a token, to the user, in order to be able to reset
212
+ * the password.
213
+ */
214
+ async function confirmForgottenPasswordCode (
215
+ req : Request ,
216
+ res : Response ,
217
+ next : NextFunction
218
+ ) {
219
+ const { userId, code } : IConfirmationCode . ConfirmPasswordCodePayload =
220
+ confirmPasswordCodePayload . parse ( req . body ) ;
221
+
222
+ const user = await users . findById ( userId ) ;
223
+ if ( ! user ) return next ( notfound . user ( ) ) ;
224
+
225
+ // Check if the code is valid (exists in the db)
226
+ const found = (
227
+ await confirmationCodes . find ( {
228
+ userId : user . id ,
229
+ purpose : IConfirmationCode . Purpose . ResetPassword ,
230
+ } )
231
+ ) [ 0 ] ;
232
+
233
+ // TODO: count the number of tries, then block using this handler for while
234
+ // for this specific userId, after a specific number of tries.
235
+ if ( ! found || found . code !== code ) return next ( invalidVerificationCode ( ) ) ;
236
+
237
+ // Ensure the code is not expired
238
+ if ( dayjs . utc ( found . expiresAt ) . isBefore ( dayjs . utc ( ) ) ) {
239
+ await confirmationCodes . deleteById ( { id : found . id } ) ;
240
+ return next ( expiredVerificationCode ( ) ) ;
241
+ }
242
+
243
+ // This token should be used by frontend reset password page
244
+ // in order to be able to change the password
245
+ const token = encodeAuthJwt ( userId , jwtSecret ) ;
246
+
247
+ // Remove the confirmation code for more data integridy and security
248
+ await confirmationCodes . deleteById ( { id : found . id } ) ;
249
+
250
+ const response : IConfirmationCode . ConfirmPasswordCodeApiResponse = { token } ;
251
+ res . status ( 200 ) . json ( response ) ;
252
+ }
253
+
254
+ async function sendEmailVerificationCode (
255
+ req : Request ,
256
+ res : Response ,
257
+ next : NextFunction
258
+ ) {
259
+ const user = req . user ;
260
+ const allowed = isUser ( user ) ;
261
+ if ( ! allowed ) return next ( forbidden ( ) ) ;
262
+
263
+ if ( user . verifiedEmail ) return next ( emailAlreadyVerified ( ) ) ;
264
+
265
+ // Generate and store the new code in the db
266
+ const { code } = await confirmationCodes . create ( {
267
+ userId : user . id ,
268
+ purpose : IConfirmationCode . Purpose . VerifyEmail ,
269
+ code : generateConfirmationCode ( ) ,
270
+ expiresAt : dayjs
271
+ . utc ( )
272
+ . add ( CONFIRMATION_CODE_VALIDITY_MINUTES , "minutes" )
273
+ . toISOString ( ) ,
274
+ } ) ;
275
+
276
+ sendBackgroundMessage ( {
277
+ type : "send-user-verification-code-email" ,
278
+ payload : {
279
+ code,
280
+ email : user . email ,
281
+ } ,
282
+ } ) ;
283
+
284
+ res . status ( 200 ) . send ( ) ;
285
+ }
286
+
287
+ /**
288
+ * Despite the name, this function verify the email in the db as well
289
+ */
290
+ async function confirmEmailVerificationCode (
291
+ req : Request ,
292
+ res : Response ,
293
+ next : NextFunction
294
+ ) {
295
+ const user = req . user ;
296
+ const allowed = isUser ( user ) ;
297
+ if ( ! allowed ) return next ( forbidden ( ) ) ;
298
+
299
+ if ( user . verifiedEmail ) return next ( emailAlreadyVerified ( ) ) ;
300
+
301
+ // get the code from the payload and verify it
302
+ const { code } : IConfirmationCode . VerifyEmailPayload =
303
+ verifyEmailPayload . parse ( req . body ) ;
304
+
305
+ const found = (
306
+ await confirmationCodes . find ( {
307
+ userId : user . id ,
308
+ purpose : IConfirmationCode . Purpose . VerifyEmail ,
309
+ } )
310
+ ) [ 0 ] ;
311
+
312
+ // TODO: count the number of tries, then block using this handler for while
313
+ // for this specific userId, after a specific number of tries.
314
+ if ( ! found || found . code !== code ) return next ( invalidVerificationCode ( ) ) ;
315
+
316
+ // Ensure the code is not expired
317
+ if ( dayjs . utc ( found . expiresAt ) . isBefore ( dayjs . utc ( ) ) ) {
318
+ await confirmationCodes . deleteById ( { id : found . id } ) ;
319
+ return next ( expiredVerificationCode ( ) ) ;
320
+ }
321
+
322
+ // update the database to mark the email as verified
323
+ await users . update ( user . id , { verifiedEmail : true } ) ;
324
+
325
+ res . status ( 200 ) . send ( ) ;
326
+ }
327
+
151
328
export default {
152
329
sendVerifyPhoneCode : safeRequest ( sendVerifyPhoneCode ) ,
153
330
verifyPhoneCode : safeRequest ( verifyPhoneCode ) ,
331
+
332
+ sendForgottenPasswordCode : safeRequest ( sendForgottenPasswordCode ) ,
333
+ confirmForgottenPasswordCode : safeRequest ( confirmForgottenPasswordCode ) ,
334
+
335
+ sendEmailVerificationCode : safeRequest ( sendEmailVerificationCode ) ,
336
+ confirmEmailVerificationCode : safeRequest ( confirmEmailVerificationCode ) ,
154
337
} ;
0 commit comments