@rpcbase/server 0.75.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/express/index.js CHANGED
@@ -29,7 +29,7 @@ module.exports = () => {
29
29
  res.set("Cache-Control", "no-store")
30
30
  next()
31
31
  })
32
-
32
+ console.log("\nwowowowow\nwowowoowo\nlfdkfdskfjlkfjdskfjds\n\n")
33
33
  // CORS
34
34
  const cors_origins = is_production ?
35
35
  // https://stackoverflow.com/questions/14003332/access-control-allow-origin-wildcard-subdomains-ports-and-protocols
@@ -43,6 +43,7 @@ module.exports = () => {
43
43
  // local dev origins
44
44
  [
45
45
  `http://localhost:${CLIENT_PORT}`,
46
+ `http://admin.localhost:${CLIENT_PORT}`,
46
47
  // WARNING: hardcoded port
47
48
  "http://localhost:8090", // TMP: used by inspected app from admin
48
49
  "http://localhost:9292", // TMP
package/mailer/index.js CHANGED
@@ -3,11 +3,12 @@ const postmark = require("postmark")
3
3
 
4
4
  const {POSTMARK_API_KEY, IS_PRODUCTION} = process.env
5
5
 
6
- const is_production = IS_PRODUCTION === "yes"
6
+ const is_production = true //IS_PRODUCTION === "yes"
7
+ console.warn("email sender forcing is production")
7
8
 
8
9
  let client
9
10
 
10
- if (is_production) {
11
+ if (is_production && typeof POSTMARK_API_KEY === "string" && POSTMARK_API_KEY.trim() !== "") {
11
12
  client = new postmark.ServerClient(POSTMARK_API_KEY)
12
13
  } else {
13
14
  client = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.75.0",
3
+ "version": "0.78.0",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -20,9 +20,10 @@
20
20
  "express": "4.18.1",
21
21
  "express-session": "1.17.3",
22
22
  "glob": "8.0.3",
23
+ "lodash": "4.17.21",
23
24
  "mongoose": "6.4.0",
24
25
  "picocolors": "1.0.0",
25
- "postmark": "3.0.11",
26
+ "postmark": "3.0.12",
26
27
  "redis": "4.1.0",
27
28
  "validator": "13.7.0",
28
29
  "yargs": "17.5.1"
@@ -0,0 +1,515 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="x-apple-disable-message-reformatting" />
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7
+ <meta name="color-scheme" content="light dark" />
8
+ <meta name="supported-color-schemes" content="light dark" />
9
+ <title></title>
10
+ <style type="text/css" rel="stylesheet" media="all">
11
+ /* Base ------------------------------ */
12
+
13
+ @import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
14
+ body {
15
+ width: 100% !important;
16
+ height: 100%;
17
+ margin: 0;
18
+ -webkit-text-size-adjust: none;
19
+ }
20
+
21
+ a {
22
+ color: #3869D4;
23
+ }
24
+
25
+ a img {
26
+ border: none;
27
+ }
28
+
29
+ td {
30
+ word-break: break-word;
31
+ }
32
+
33
+ .preheader {
34
+ display: none !important;
35
+ visibility: hidden;
36
+ mso-hide: all;
37
+ font-size: 1px;
38
+ line-height: 1px;
39
+ max-height: 0;
40
+ max-width: 0;
41
+ opacity: 0;
42
+ overflow: hidden;
43
+ }
44
+ /* Type ------------------------------ */
45
+
46
+ body,
47
+ td,
48
+ th {
49
+ font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
50
+ }
51
+
52
+ h1 {
53
+ margin-top: 0;
54
+ color: #333333;
55
+ font-size: 22px;
56
+ font-weight: bold;
57
+ text-align: left;
58
+ }
59
+
60
+ h2 {
61
+ margin-top: 0;
62
+ color: #333333;
63
+ font-size: 16px;
64
+ font-weight: bold;
65
+ text-align: left;
66
+ }
67
+
68
+ h3 {
69
+ margin-top: 0;
70
+ color: #333333;
71
+ font-size: 14px;
72
+ font-weight: bold;
73
+ text-align: left;
74
+ }
75
+
76
+ td,
77
+ th {
78
+ font-size: 16px;
79
+ }
80
+
81
+ p,
82
+ ul,
83
+ ol,
84
+ blockquote {
85
+ margin: .4em 0 1.1875em;
86
+ font-size: 16px;
87
+ line-height: 1.625;
88
+ }
89
+
90
+ p.sub {
91
+ font-size: 13px;
92
+ }
93
+ /* Utilities ------------------------------ */
94
+
95
+ .align-right {
96
+ text-align: right;
97
+ }
98
+
99
+ .align-left {
100
+ text-align: left;
101
+ }
102
+
103
+ .align-center {
104
+ text-align: center;
105
+ }
106
+ /* Buttons ------------------------------ */
107
+
108
+ .button {
109
+ background-color: #3869D4;
110
+ border-top: 10px solid #3869D4;
111
+ border-right: 18px solid #3869D4;
112
+ border-bottom: 10px solid #3869D4;
113
+ border-left: 18px solid #3869D4;
114
+ display: inline-block;
115
+ color: #FFF;
116
+ text-decoration: none;
117
+ border-radius: 3px;
118
+ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
119
+ -webkit-text-size-adjust: none;
120
+ box-sizing: border-box;
121
+ }
122
+
123
+ .button--green {
124
+ background-color: #22BC66;
125
+ border-top: 10px solid #22BC66;
126
+ border-right: 18px solid #22BC66;
127
+ border-bottom: 10px solid #22BC66;
128
+ border-left: 18px solid #22BC66;
129
+ }
130
+
131
+ .button--red {
132
+ background-color: #FF6136;
133
+ border-top: 10px solid #FF6136;
134
+ border-right: 18px solid #FF6136;
135
+ border-bottom: 10px solid #FF6136;
136
+ border-left: 18px solid #FF6136;
137
+ }
138
+
139
+ @media only screen and (max-width: 500px) {
140
+ .button {
141
+ width: 100% !important;
142
+ text-align: center !important;
143
+ }
144
+ }
145
+ /* Attribute list ------------------------------ */
146
+
147
+ .attributes {
148
+ margin: 0 0 21px;
149
+ }
150
+
151
+ .attributes_content {
152
+ background-color: #F4F4F7;
153
+ padding: 16px;
154
+ }
155
+
156
+ .attributes_item {
157
+ padding: 0;
158
+ }
159
+ /* Related Items ------------------------------ */
160
+
161
+ .related {
162
+ width: 100%;
163
+ margin: 0;
164
+ padding: 25px 0 0 0;
165
+ -premailer-width: 100%;
166
+ -premailer-cellpadding: 0;
167
+ -premailer-cellspacing: 0;
168
+ }
169
+
170
+ .related_item {
171
+ padding: 10px 0;
172
+ color: #CBCCCF;
173
+ font-size: 15px;
174
+ line-height: 18px;
175
+ }
176
+
177
+ .related_item-title {
178
+ display: block;
179
+ margin: .5em 0 0;
180
+ }
181
+
182
+ .related_item-thumb {
183
+ display: block;
184
+ padding-bottom: 10px;
185
+ }
186
+
187
+ .related_heading {
188
+ border-top: 1px solid #CBCCCF;
189
+ text-align: center;
190
+ padding: 25px 0 10px;
191
+ }
192
+ /* Discount Code ------------------------------ */
193
+
194
+ .discount {
195
+ width: 100%;
196
+ margin: 0;
197
+ padding: 24px;
198
+ -premailer-width: 100%;
199
+ -premailer-cellpadding: 0;
200
+ -premailer-cellspacing: 0;
201
+ background-color: #F4F4F7;
202
+ border: 2px dashed #CBCCCF;
203
+ }
204
+
205
+ .discount_heading {
206
+ text-align: center;
207
+ }
208
+
209
+ .discount_body {
210
+ text-align: center;
211
+ font-size: 15px;
212
+ }
213
+ /* Social Icons ------------------------------ */
214
+
215
+ .social {
216
+ width: auto;
217
+ }
218
+
219
+ .social td {
220
+ padding: 0;
221
+ width: auto;
222
+ }
223
+
224
+ .social_icon {
225
+ height: 20px;
226
+ margin: 0 8px 10px 8px;
227
+ padding: 0;
228
+ }
229
+ /* Data table ------------------------------ */
230
+
231
+ .purchase {
232
+ width: 100%;
233
+ margin: 0;
234
+ padding: 35px 0;
235
+ -premailer-width: 100%;
236
+ -premailer-cellpadding: 0;
237
+ -premailer-cellspacing: 0;
238
+ }
239
+
240
+ .purchase_content {
241
+ width: 100%;
242
+ margin: 0;
243
+ padding: 25px 0 0 0;
244
+ -premailer-width: 100%;
245
+ -premailer-cellpadding: 0;
246
+ -premailer-cellspacing: 0;
247
+ }
248
+
249
+ .purchase_item {
250
+ padding: 10px 0;
251
+ color: #51545E;
252
+ font-size: 15px;
253
+ line-height: 18px;
254
+ }
255
+
256
+ .purchase_heading {
257
+ padding-bottom: 8px;
258
+ border-bottom: 1px solid #EAEAEC;
259
+ }
260
+
261
+ .purchase_heading p {
262
+ margin: 0;
263
+ color: #85878E;
264
+ font-size: 12px;
265
+ }
266
+
267
+ .purchase_footer {
268
+ padding-top: 15px;
269
+ border-top: 1px solid #EAEAEC;
270
+ }
271
+
272
+ .purchase_total {
273
+ margin: 0;
274
+ text-align: right;
275
+ font-weight: bold;
276
+ color: #333333;
277
+ }
278
+
279
+ .purchase_total--label {
280
+ padding: 0 15px 0 0;
281
+ }
282
+
283
+ body {
284
+ background-color: #F4F4F7;
285
+ color: #51545E;
286
+ }
287
+
288
+ p {
289
+ color: #51545E;
290
+ }
291
+
292
+ p.sub {
293
+ color: #6B6E76;
294
+ }
295
+
296
+ .email-wrapper {
297
+ width: 100%;
298
+ margin: 0;
299
+ padding: 0;
300
+ -premailer-width: 100%;
301
+ -premailer-cellpadding: 0;
302
+ -premailer-cellspacing: 0;
303
+ background-color: #F4F4F7;
304
+ }
305
+
306
+ .email-content {
307
+ width: 100%;
308
+ margin: 0;
309
+ padding: 0;
310
+ -premailer-width: 100%;
311
+ -premailer-cellpadding: 0;
312
+ -premailer-cellspacing: 0;
313
+ }
314
+ /* Masthead ----------------------- */
315
+
316
+ .email-masthead {
317
+ padding: 25px 0;
318
+ text-align: center;
319
+ }
320
+
321
+ .email-masthead_logo {
322
+ width: 94px;
323
+ }
324
+
325
+ .email-masthead_name {
326
+ font-size: 16px;
327
+ font-weight: bold;
328
+ color: #A8AAAF;
329
+ text-decoration: none;
330
+ text-shadow: 0 1px 0 white;
331
+ }
332
+ /* Body ------------------------------ */
333
+
334
+ .email-body {
335
+ width: 100%;
336
+ margin: 0;
337
+ padding: 0;
338
+ -premailer-width: 100%;
339
+ -premailer-cellpadding: 0;
340
+ -premailer-cellspacing: 0;
341
+ background-color: #FFFFFF;
342
+ }
343
+
344
+ .email-body_inner {
345
+ width: 570px;
346
+ margin: 0 auto;
347
+ padding: 0;
348
+ -premailer-width: 570px;
349
+ -premailer-cellpadding: 0;
350
+ -premailer-cellspacing: 0;
351
+ background-color: #FFFFFF;
352
+ }
353
+
354
+ .email-footer {
355
+ width: 570px;
356
+ margin: 0 auto;
357
+ padding: 0;
358
+ -premailer-width: 570px;
359
+ -premailer-cellpadding: 0;
360
+ -premailer-cellspacing: 0;
361
+ text-align: center;
362
+ }
363
+
364
+ .email-footer p {
365
+ color: #6B6E76;
366
+ }
367
+
368
+ .body-action {
369
+ width: 100%;
370
+ margin: 30px auto;
371
+ padding: 0;
372
+ -premailer-width: 100%;
373
+ -premailer-cellpadding: 0;
374
+ -premailer-cellspacing: 0;
375
+ text-align: center;
376
+ }
377
+
378
+ .body-sub {
379
+ margin-top: 25px;
380
+ padding-top: 25px;
381
+ border-top: 1px solid #EAEAEC;
382
+ }
383
+
384
+ .content-cell {
385
+ padding: 35px;
386
+ }
387
+ /*Media Queries ------------------------------ */
388
+
389
+ @media only screen and (max-width: 600px) {
390
+ .email-body_inner,
391
+ .email-footer {
392
+ width: 100% !important;
393
+ }
394
+ }
395
+
396
+ @media (prefers-color-scheme: dark) {
397
+ body,
398
+ .email-body,
399
+ .email-body_inner,
400
+ .email-content,
401
+ .email-wrapper,
402
+ .email-masthead,
403
+ .email-footer {
404
+ background-color: #333333 !important;
405
+ color: #FFF !important;
406
+ }
407
+ p,
408
+ ul,
409
+ ol,
410
+ blockquote,
411
+ h1,
412
+ h2,
413
+ h3,
414
+ span,
415
+ .purchase_item {
416
+ color: #FFF !important;
417
+ }
418
+ .attributes_content,
419
+ .discount {
420
+ background-color: #222 !important;
421
+ }
422
+ .email-masthead_name {
423
+ text-shadow: none !important;
424
+ }
425
+ }
426
+
427
+ :root {
428
+ color-scheme: light dark;
429
+ supported-color-schemes: light dark;
430
+ }
431
+ </style>
432
+ <!--[if mso]>
433
+ <style type="text/css">
434
+ .f-fallback {
435
+ font-family: Arial, sans-serif;
436
+ }
437
+ </style>
438
+ <![endif]-->
439
+ </head>
440
+ <body>
441
+ <span class="preheader">Your password reset link is ready</span>
442
+ <table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
443
+ <tr>
444
+ <td align="center">
445
+ <table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
446
+ <tr>
447
+ <td class="email-masthead">
448
+ <a href="https://example.com" class="f-fallback email-masthead_name">
449
+ [Product Name]
450
+ </a>
451
+ </td>
452
+ </tr>
453
+ <!-- Email Body -->
454
+ <tr>
455
+ <td class="email-body" width="100%" cellpadding="0" cellspacing="0">
456
+ <table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
457
+ <!-- Body content -->
458
+ <tr>
459
+ <td class="content-cell">
460
+ <div class="f-fallback">
461
+ <h1>Your password reset link</h1>
462
+ <p>The password reset link you have requested is ready. If you did not request a password reset, ignore this email.</p>
463
+ <!-- Action -->
464
+ <table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
465
+ <tr>
466
+ <td align="center">
467
+ <!-- Border based button
468
+ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
469
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
470
+ <tr>
471
+ <td align="center">
472
+ <a href="<%= reset_url %>" class="f-fallback button" target="_blank">Reset My Password</a>
473
+ </td>
474
+ </tr>
475
+ </table>
476
+ </td>
477
+ </tr>
478
+ </table>
479
+
480
+ <table class="body-sub" role="presentation">
481
+ <tr>
482
+ <td>
483
+ <p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
484
+ <p class="f-fallback sub"><%= reset_url %></p>
485
+ </td>
486
+ </tr>
487
+ </table>
488
+ </div>
489
+ </td>
490
+ </tr>
491
+ </table>
492
+ </td>
493
+ </tr>
494
+ <tr>
495
+ <td>
496
+ <table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
497
+ <tr>
498
+ <td class="content-cell" align="center">
499
+ <p class="f-fallback sub align-center">&copy; 2022 [Product Name]. All rights reserved.</p>
500
+ <p class="f-fallback sub align-center">
501
+ [Company Name, LLC]
502
+ <br>1234 Street Rd.
503
+ <br>Suite 1234
504
+ </p>
505
+ </td>
506
+ </tr>
507
+ </table>
508
+ </td>
509
+ </tr>
510
+ </table>
511
+ </td>
512
+ </tr>
513
+ </table>
514
+ </body>
515
+ </html>
package/src/auth/index.js CHANGED
@@ -4,6 +4,8 @@ const async_wrapper = require("../helpers/async_wrapper")
4
4
  const sign_up = require("./sign_up")
5
5
  const sign_in = require("./sign_in")
6
6
  const sign_out = require("./sign_out")
7
+ const reset_password = require("./reset_password")
8
+ const set_new_password = require("./set_new_password")
7
9
  const check_session = require("./check_session")
8
10
 
9
11
  const sign_up_handler = async(req, res) => {
@@ -21,6 +23,16 @@ const sign_out_handler = async(req, res) => {
21
23
  res.json(result)
22
24
  }
23
25
 
26
+ const reset_password_handler = async(req, res) => {
27
+ const result = await reset_password(req.body, {req})
28
+ res.json(result)
29
+ }
30
+
31
+ const set_new_password_handler = async(req, res) => {
32
+ const result = await set_new_password(req.body, {req})
33
+ res.json(result)
34
+ }
35
+
24
36
  const check_session_handler = async(req, res) => {
25
37
  const result = await check_session(req.body, {req})
26
38
  res.json(result)
@@ -31,5 +43,8 @@ module.exports = (app) => {
31
43
  app.post("/api/v1/auth/sign_in", async_wrapper(sign_in_handler))
32
44
  app.post("/api/v1/auth/sign_out", async_wrapper(sign_out_handler))
33
45
 
46
+ app.post("/api/v1/auth/reset_password", async_wrapper(reset_password_handler))
47
+ app.post("/api/v1/auth/set_new_password", async_wrapper(set_new_password_handler))
48
+
34
49
  app.post("/api/v1/auth/check_session", async_wrapper(check_session_handler))
35
50
  }
@@ -0,0 +1,70 @@
1
+ /* @flow */
2
+ const _template = require("lodash/template")
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+ const isEmail = require("validator/lib/isEmail")
6
+
7
+ const {hash_password, compare_hash} = require("@rpcbase/std/crypto/hash")
8
+ const get_random_str = require("@rpcbase/std/crypto/get_random_str")
9
+
10
+ const mailer = require("../../mailer")
11
+ const mongoose = require("../../mongoose")
12
+ const ResetPasswordToken = require("../models/ResetPasswordToken")
13
+
14
+
15
+ const {APP_DOMAIN} = process.env
16
+
17
+ const email_tpl = _template(
18
+ fs.readFileSync(path.join(__dirname, "./forgot_password_email.html"), "utf8")
19
+ )
20
+
21
+
22
+ const reset_password = async({email}, ctx) => {
23
+ const User = mongoose.model("User")
24
+
25
+ if (!isEmail(email)) {
26
+ throw new Error("reset_password:: invalid email")
27
+ }
28
+
29
+ const user = await User.findOne({email}, null, {ctx})
30
+
31
+ if (!user) {
32
+ // TODO: add random delay to prevent detecting if account exists based on response time
33
+ return {status: "ok"}
34
+ }
35
+
36
+ const token = get_random_str(32)
37
+ const token_hash = await hash_password(token)
38
+
39
+ console.log("WOWOOWOWOW", await compare_hash(token, token_hash))
40
+
41
+ console.log("token", token)
42
+ console.log("token_hash", token_hash)
43
+
44
+ const reset_token = new ResetPasswordToken({
45
+ user_id: user._id,
46
+ token_hash,
47
+ })
48
+
49
+ await reset_token.save()
50
+
51
+ const reset_url = `https://${APP_DOMAIN}/set-new-password?id=${user._id}&token=${token}`
52
+
53
+ const res = await mailer.sendEmail({
54
+ From: "hello@commit-queue.com",
55
+ To: user.email,
56
+ Subject: "Your password reset link",
57
+ HtmlBody: email_tpl({
58
+ reset_url,
59
+ }),
60
+ })
61
+
62
+ // cleanup if email wasn't sent
63
+ if (res.Message !== "OK") {
64
+ await reset_token.delete()
65
+ }
66
+
67
+ return {status: "ok"}
68
+ }
69
+
70
+ module.exports = reset_password
@@ -0,0 +1,63 @@
1
+ /* @flow */
2
+ const _template = require("lodash/template")
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+ const isEmail = require("validator/lib/isEmail")
6
+
7
+ const {hash_password, compare_hash} = require("@rpcbase/std/crypto/hash")
8
+ const get_random_str = require("@rpcbase/std/crypto/get_random_str")
9
+
10
+ const mailer = require("../../mailer")
11
+ const mongoose = require("../../mongoose")
12
+ const ResetPasswordToken = require("../models/ResetPasswordToken")
13
+
14
+ const {APP_DOMAIN} = process.env
15
+
16
+ const email_tpl = _template(
17
+ fs.readFileSync(path.join(__dirname, "./set_new_password_email.html"), "utf8")
18
+ )
19
+
20
+ const set_new_password = async({user_id, token, password}, ctx) => {
21
+ const User = mongoose.model("User")
22
+
23
+ const reset_tokens = await ResetPasswordToken.find({user_id})
24
+ .limit(100)
25
+
26
+ if (reset_tokens.length === 0) {
27
+ return {
28
+ message: "Unable to validate reset token, please try again in a moment"
29
+ }
30
+ }
31
+
32
+ let is_match = false
33
+
34
+ for (let i = 0; i < reset_tokens.length; i++) {
35
+ const op = await compare_hash(token, reset_tokens[i].token_hash)
36
+ if (op) {
37
+ is_match = true
38
+ break
39
+ }
40
+ }
41
+
42
+ if (!is_match) {
43
+ return {
44
+ message: "Invalid or expired token, please try again in a moment"
45
+ }
46
+ }
47
+
48
+ // token is valid, update the user password
49
+ const user = await User.findOne({_id: user_id}, null, {ctx})
50
+ const hashed_password = await hash_password(password)
51
+ user.password_hash = hashed_password
52
+
53
+ await user.save()
54
+
55
+ // WARNING:
56
+ // TODO: important! notify the user that their password has been reset
57
+
58
+ return {
59
+ status: "ok"
60
+ }
61
+ }
62
+
63
+ module.exports = set_new_password
@@ -0,0 +1,3 @@
1
+ todo: implement notification email
2
+
3
+ your password has been reset
@@ -17,7 +17,6 @@ const sign_in = async({email, password}, ctx) => {
17
17
  // const user = await User.findOne({email}, null, {ctx})
18
18
  const user = await User.findOne({email}, null)
19
19
 
20
- // console.log("ICII2222", email, password, user)
21
20
 
22
21
  if (!user) {
23
22
  return fail()
@@ -25,9 +24,6 @@ const sign_in = async({email, password}, ctx) => {
25
24
 
26
25
  const hashed_pass = user.password_hash
27
26
 
28
- // TODO: fix broken bcrypt bins in docker
29
- // const is_match = await bcrypt.compare(password, hashed_pass)
30
- console.warn("warning: skipping bcrypt hash check")
31
27
  const is_match = await compare_hash(password, hashed_pass)
32
28
 
33
29
  if (is_match) {
@@ -0,0 +1,14 @@
1
+ /* @flow */
2
+ const mongoose = require("../../mongoose")
3
+
4
+ const ResetPasswordToken = mongoose.model("ResetPasswordToken", {
5
+ user_id: String,
6
+ token_hash: String,
7
+ created_at: {
8
+ type: Date,
9
+ expires: 360, // 6min
10
+ default: Date.now,
11
+ }
12
+ })
13
+
14
+ module.exports = ResetPasswordToken