@rpcbase/server 0.73.0 → 0.76.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/admin/README.md CHANGED
@@ -1 +1 @@
1
- admin middleware that exposes models, schemas etc
1
+ admin middleware that exposes the local models, schemas etc
package/bin.js CHANGED
@@ -6,13 +6,13 @@ require("dotenv").config({path: path.join(__dirname, "./.env")})
6
6
  const yargs = require("yargs/yargs")
7
7
  const {hideBin} = require("yargs/helpers")
8
8
 
9
+ const pack = require("./package.json")
9
10
  const start_server_infrastructure = require("./cli/start_server_infrastructure")
10
11
  const build_server = require("./cli/build_server")
11
12
  const run_agent = require("./cli/run_agent")
12
13
 
13
14
  let is_command = false
14
15
 
15
-
16
16
  const args = yargs(hideBin(process.argv))
17
17
  .command("start", "runs server/infrastructure", () => {}, (args) => {
18
18
  is_command = true
@@ -22,12 +22,11 @@ const args = yargs(hideBin(process.argv))
22
22
  is_command = true
23
23
  run_agent()
24
24
  })
25
- .command("build", "build server", () => {}, (args) => {
25
+ .command("build", "Build the server package", () => {}, (args) => {
26
26
  is_command = true
27
27
  build_server()
28
28
  })
29
29
  .option("verbose", {
30
- alias: "v",
31
30
  type: "boolean",
32
31
  default: false,
33
32
  description: "Run with verbose logging"
@@ -38,6 +37,8 @@ if (!is_command) {
38
37
  console.log("server default com22mand")
39
38
  }
40
39
 
40
+
41
+ // TODO: why was this needed
41
42
  // // add helpers to ensure proper process termination
42
43
  // const exit_clean = () => {
43
44
  // process.exit()
@@ -1,10 +1,10 @@
1
1
  /* @flow */
2
+ // TODO: replace console debug
2
3
 
3
- const is_production = process.env.NODE_ENV === "production"
4
4
 
5
+ const is_production = process.env.NODE_ENV === "production"
5
6
 
6
7
  module.exports = (app) => {
7
-
8
8
  app.post("/api/__dev_save_coverage", (req, res) => {
9
9
  console.log("sending coverage for", req.body.path_key)
10
10
  res.json(global.__coverage__)
package/express/index.js CHANGED
@@ -3,9 +3,13 @@ const cors = require("cors")
3
3
  const express = require("express")
4
4
  const body_parser = require("body-parser")
5
5
 
6
+ // functionality middlewares
7
+ const auth = require("../src/auth")
8
+
6
9
  const dev_save_coverage = require("./dev_save_coverage")
7
10
  const session_middleware = require("./session_middleware")
8
11
 
12
+
9
13
  const is_production = process.env.IS_PRODUCTION === "yes"
10
14
  const {APP_DOMAIN, CLIENT_PORT} = process.env
11
15
 
@@ -57,6 +61,7 @@ module.exports = () => {
57
61
  app.get("/api/ping", (req, res) => res.json({message: "pong"}))
58
62
  app.post("/api/ping", (req, res) => res.json({message: "pong"}))
59
63
 
64
+ auth(app)
60
65
  dev_save_coverage(app)
61
66
 
62
67
  return app
@@ -58,9 +58,14 @@ setTimeout(async() => {
58
58
  store: new redis_store({client: redis_client}),
59
59
  proxy: true,
60
60
  saveUninitialized: false,
61
+ // WARNING
61
62
  // TODO: use session secret from env
62
63
  secret: "session secret wowow",
63
64
  resave: false,
65
+ cookie: {
66
+ // TODO: test this
67
+ maxAge: 1000 * 3600 * 24 * 100 // 100 days
68
+ }
64
69
  })
65
70
 
66
71
  // TODO: is this still necessary
package/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /* @flow */
2
- const client_router = require("./client/client_router")
3
2
  const database = require("./database")
4
3
  const express = require("./express")
5
- const rpc_router = require("./rpc/rpc_router")
4
+ const client_router = require("./src/client/client_router")
5
+ const rpc_router = require("./src/rpc/rpc_router")
6
6
 
7
7
  module.exports = {
8
- client_router,
9
8
  database,
10
9
  express,
10
+ client_router,
11
11
  rpc_router,
12
12
  }
package/mailer/index.js CHANGED
@@ -3,7 +3,8 @@ 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.73.0",
3
+ "version": "0.76.0",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@rpcbase/agent": "0.9.0",
14
+ "@rpcbase/std": "0.3.0",
14
15
  "body-parser": "1.20.0",
15
16
  "connect-redis": "6.1.3",
16
17
  "cors": "2.8.5",
@@ -0,0 +1,15 @@
1
+ /* @flow */
2
+
3
+ const check_session = async(payload, ctx) => {
4
+ const {req} = ctx
5
+ // console.log("session check", req.session)
6
+
7
+ const is_signed_in = !!req.session.user_id
8
+ return {
9
+ status: "ok",
10
+ is_signed_in,
11
+ user_id: req.session.user_id,
12
+ }
13
+ }
14
+
15
+ module.exports = check_session
@@ -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>
@@ -0,0 +1,50 @@
1
+ /* @flow */
2
+ const async_wrapper = require("../helpers/async_wrapper")
3
+
4
+ const sign_up = require("./sign_up")
5
+ const sign_in = require("./sign_in")
6
+ const sign_out = require("./sign_out")
7
+ const reset_password = require("./reset_password")
8
+ const set_new_password = require("./set_new_password")
9
+ const check_session = require("./check_session")
10
+
11
+ const sign_up_handler = async(req, res) => {
12
+ const result = await sign_up(req.body, {req})
13
+ res.json(result)
14
+ }
15
+
16
+ const sign_in_handler = async(req, res) => {
17
+ const result = await sign_in(req.body, {req})
18
+ res.json(result)
19
+ }
20
+
21
+ const sign_out_handler = async(req, res) => {
22
+ const result = await sign_out(req.body, {req})
23
+ res.json(result)
24
+ }
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
+
36
+ const check_session_handler = async(req, res) => {
37
+ const result = await check_session(req.body, {req})
38
+ res.json(result)
39
+ }
40
+
41
+ module.exports = (app) => {
42
+ app.post("/api/v1/auth/sign_up", async_wrapper(sign_up_handler))
43
+ app.post("/api/v1/auth/sign_in", async_wrapper(sign_in_handler))
44
+ app.post("/api/v1/auth/sign_out", async_wrapper(sign_out_handler))
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
+
49
+ app.post("/api/v1/auth/check_session", async_wrapper(check_session_handler))
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
@@ -0,0 +1,40 @@
1
+ /* @flow */
2
+ const {compare_hash} = require("@rpcbase/std/crypto/hash")
3
+
4
+ const mongoose = require("../../mongoose")
5
+
6
+ const fail = () => ({
7
+ errors: {form: "Invalid email or password"}
8
+ })
9
+
10
+ const sign_in = async({email, password}, ctx) => {
11
+ const User = mongoose.model("User")
12
+
13
+ const {req} = ctx
14
+
15
+ // find the matching user
16
+ // TODO: document ctx param
17
+ // const user = await User.findOne({email}, null, {ctx})
18
+ const user = await User.findOne({email}, null)
19
+
20
+
21
+ if (!user) {
22
+ return fail()
23
+ }
24
+
25
+ const hashed_pass = user.password_hash
26
+
27
+ const is_match = await compare_hash(password, hashed_pass)
28
+
29
+ if (is_match) {
30
+ req.session.user_id = user._id.toString()
31
+ await req.session.save()
32
+ return {
33
+ status: "ok"
34
+ }
35
+ } else {
36
+ return fail()
37
+ }
38
+ }
39
+
40
+ module.exports = sign_in
@@ -0,0 +1,11 @@
1
+ /* @flow */
2
+
3
+ const sign_out = async(payload, ctx) => {
4
+ const {req} = ctx
5
+ await req.session.destroy()
6
+ return {
7
+ status: "ok"
8
+ }
9
+ }
10
+
11
+ module.exports = sign_out
@@ -0,0 +1,57 @@
1
+ /* @flow */
2
+ const {hash_password} = require("@rpcbase/std/crypto/hash")
3
+
4
+ const mongoose = require("../../mongoose")
5
+
6
+ const sign_up = async({email, password}, ctx) => {
7
+ const User = mongoose.model("User")
8
+ const Invite = mongoose.model("Invite")
9
+
10
+ const {req} = ctx
11
+
12
+ // check if the user already exists
13
+ const existing_user = await User.findOne({email}, null, {ctx})
14
+
15
+ if (existing_user) {
16
+ return {
17
+ status: "error",
18
+ message: "User already exists"
19
+ }
20
+ }
21
+
22
+ // check if we have an invite for this user
23
+ const invite = await Invite.findOne({email}, null, {ctx})
24
+ if (invite && !invite.is_ready) {
25
+ console.log("found an invite, but not ready", email)
26
+ return {
27
+ status: "error",
28
+ message: "Your invite is still pending approval. Expect an email in the next weeks to activate your account."
29
+ }
30
+ } else if (!invite) {
31
+ console.log("no invite for signup email:", email)
32
+ return {
33
+ status: "error",
34
+ message: "No valid invite was found for this email"
35
+ }
36
+ }
37
+
38
+ const hash = await hash_password(password)
39
+
40
+ const user = new User({
41
+ email,
42
+ password_hash: hash
43
+ })
44
+
45
+ // sign the user in
46
+ req.session.user_id = user._id.toString()
47
+
48
+ await req.session.save()
49
+ await user.save({ctx})
50
+
51
+ return {
52
+ status: "ok",
53
+ user_id: user._id
54
+ }
55
+ }
56
+
57
+ module.exports = sign_up
@@ -0,0 +1,2 @@
1
+ client router serves the client from the server
2
+ in production the client could be served from the CDN, but the server is also suitable
@@ -3,6 +3,8 @@ const fs = require("fs")
3
3
  const path = require("path")
4
4
  const glob = require("glob")
5
5
 
6
+ const async_wrapper = require("../helpers/async_wrapper")
7
+
6
8
  const src_path = path.join(process.cwd(), "./src/")
7
9
  const build_dir = path.join(process.cwd(), "build/")
8
10
  const client_build_dir = path.join(build_dir, "./client")
@@ -10,11 +12,6 @@ const client_build_dir = path.join(build_dir, "./client")
10
12
 
11
13
  // TODO: add build time static assets compression
12
14
 
13
- const async_wrapper = fn => (req, res, next) => {
14
- Promise.resolve(fn(req, res, next))
15
- .catch(next)
16
- }
17
-
18
15
  const get_client_routes = () => {
19
16
  const client_files = glob.sync(path.join(client_build_dir, "./**/*"))
20
17
  const routes = client_files
@@ -0,0 +1,8 @@
1
+ /* @flow */
2
+
3
+ const async_wrapper = fn => (req, res, next) => {
4
+ Promise.resolve(fn(req, res, next))
5
+ .catch(next)
6
+ }
7
+
8
+ module.exports = async_wrapper
@@ -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
@@ -4,16 +4,12 @@ const path = require("path")
4
4
  const glob = require("glob")
5
5
  const debug = require("debug")
6
6
 
7
+ const async_wrapper = require("../helpers/async_wrapper")
8
+
7
9
  const src_path = path.join(process.cwd(), "./src/")
8
10
  const build_dir = path.join(process.cwd(), "build/")
9
11
 
10
12
 
11
- const async_wrapper = fn => (req, res, next) => {
12
- Promise.resolve(fn(req, res, next))
13
- .catch(next)
14
- }
15
-
16
-
17
13
  const rpc_router = (app) => {
18
14
  const rpc_routes = glob.sync(path.join(build_dir, "./rpc/*"))
19
15
 
package/crypto/index.js DELETED
@@ -1,45 +0,0 @@
1
- /* @flow */
2
- const crypto = require("crypto")
3
-
4
- const ALGORITHM = "aes-256-cbc"
5
- const {CRYPTO_SECRET} = process.env
6
-
7
- if (!CRYPTO_SECRET || CRYPTO_SECRET === "") {
8
- throw new Error("CRYPTO_SECRET not found in env")
9
- }
10
-
11
- const key = crypto.createHash("sha256")
12
- .update(String(CRYPTO_SECRET))
13
- .digest()
14
-
15
-
16
- const encrypt = (text) => {
17
- const iv = crypto.randomBytes(16)
18
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
19
-
20
- const encrypted = Buffer.concat([
21
- cipher.update(text),
22
- cipher.final()
23
- ])
24
-
25
- return {
26
- iv: iv.toString("hex"),
27
- content: encrypted.toString("hex")
28
- }
29
- }
30
-
31
- const decrypt = (hash) => {
32
- const decipher = crypto.createDecipheriv(
33
- ALGORITHM, key, Buffer.from(hash.iv, "hex")
34
- )
35
-
36
- const decrpyted = Buffer.concat([
37
- decipher.update(Buffer.from(hash.content, "hex")),
38
- decipher.final(),
39
- ])
40
-
41
- return decrpyted.toString()
42
- }
43
-
44
-
45
- module.exports = {encrypt, decrypt}