56 次程式碼提交

作者 SHA1 備註 提交日期
  tomit4 9966fff8ce :construction: Continued filtering logic for initial survey answers 2 年之前
  tomit4 a752ce912c :pencil2: Removed secure attribute from auth cookie 2 年之前
  tomit4 128f2b1f17 :memo: Updated readme with sql stmts for prod 2 年之前
  tomit4 c4ddeb2ae6 :construction: Updated env sample with new variables 2 年之前
  tomit4 327b3b63c2 :recycle: Refactor with new Brevo API 2 年之前
  tomit4 2ed8d01ceb :bug: Fixed bug which blocked login related to legacy token logic 2 年之前
  tomit4 df9e234db8 :pencil2: fixed typo from rebase 2 年之前
  tomit4 537eb836c1 :pencil2: Fixed merge issues 2 年之前
  tomit4 77ef364d1b :construction: Set up distance as well, refactored hasMin check 2 年之前
  tomit4 d225deeb60 :construction: Removed deleted routes from plugin registry 2 年之前
  tomit4 927846588b :boom: Removed unnecessary user routes 2 年之前
  tomit4 56875d1b0f :construction: Restructured survey to include more data points 2 年之前
  tomit4 7fb35bee0c :wrench: Adjusted config of send-email to use .env 2 年之前
  tomit4 04e17c21f9 :bug: Fixed bug with cors on login 2 年之前
  tomit4 a2f9031222 :bug: Fixed verification errors 2 年之前
  tomit4 76d442c283 :bug: Fixed validation error logs 2 年之前
  tomit4 fbecd80b29 :white_check_mark: Added test for user/validate session route 2 年之前
  tomit4 25601b3ff5 :pencil2: Fixed semantic issues and addressed edge null case 2 年之前
  tomit4 730669ecf9 :white_check_mark: Added test for user/verify-session route 2 年之前
  tomit4 83e2e0d36c :pencil2: Fixed merge issues 2 年之前
  tomit4 62f1d142a0 :white_check_mark: Added user-create-token test 2 年之前
  tomit4 a256bf3b45 :pencil2: Cleaned up a bit of user-send-email test 2 年之前
  tomit4 cde6bd825e :white_check_mark: Added user-send-email test 2 年之前
  tomit4 9e147d4b73 :pencil2: Removed old commented out code, added TODO for hasher, etc. 2 年之前
  tomit4 cbd6e2c263 :white_check_mark: Added user-remove-session test 2 年之前
  tomit4 ac485b3946 :white_check_mark: Continued writing tests for user routes 2 年之前
  tomit4 eac8a832b6 :white_check_mark: Added test for user signup 2 年之前
  tomit4 5b24d19b03 :pencil2: Fixed merge issues 2 年之前
  tomit4 7b2f797202 :pencil2: Fixed merge issues 2 年之前
  tomit4 50a647ca05 :pencil2: Fixed merge issues 2 年之前
  tomit4 8d3d92d5cf :pencil2: fixed typo 2 年之前
  tomit4 f3f0db5e59 :white_check_mark: Started testing 2 年之前
  tomit4 31c9fb499d :pencil2: Removed unnecessary route file 2 年之前
  tomit4 965de14502 :sparkles: Implemented login 2 年之前
  tomit4 e8fcddf1de :sparkles: Implemented login 2 年之前
  tomit4 1165af9dce :sparkles: Implemented logout 2 年之前
  tomit4 5b02c795aa :pencil2: Fixed merge issues 2 年之前
  tomit4 09a3e57bee :pencil2: Fixed merge conflicts 2 年之前
  tomit4 b95fc0c3a2 :bug: Fixed bug of no display responses in surveycomplete, fixed waveui guards.js bug 2 年之前
  tomit4 990b1bd010 :bug: Disabled new profile errors by adjusting mock 2 年之前
  tomit4 51a85f42a9 :construction: Small optimization in surveycompleteview logic 2 年之前
  tomit4 18c6794b9c :pencil2: Fixed merge issues 2 年之前
  tomit4 38bf91dc06 :memo: Small note on how to proceed with SurveyCompleteView 2 年之前
  tomit4 e328f039f9 :pencil2: Fixed merge issues 2 年之前
  tomit4 8e0c278c6e :pencil2: Fixed merge issues 2 年之前
  tomit4 7737961ccc :construction: Achieved basic login upon verification from email 2 年之前
  tomit4 8778b9ca8d :pencil2: Fixed merge issues 2 年之前
  tomit4 aed2c800b2 :pencil2: Fixed merge issues 2 年之前
  tomit4 07b9d52e6b :construction: Small optimization in surveycompleteview logic 2 年之前
  tomit4 9672ec967d :pencil2: Fixed merge conflicts 2 年之前
  tomit4 6275f81892 :memo: Small note on how to proceed with SurveyCompleteView 2 年之前
  tomit4 404d2c0384 :construction: Small optimization for authenticator, note in guards.js 2 年之前
  tomit4 d42089f8ec :construction: Achieved basic login upon verification from email 2 年之前
  tomit4 38ee41df0a :construction: Achieved basic login upon verification from email 2 年之前
  tomit4 bcbcae1170 :memo: Note of where to put next piece of logic 2 年之前
  tomit4 77a104ae3c :construction: Began connecting survey to Home of app 2 年之前
共有 55 個文件被更改,包括 1970 次插入673 次删除
  1. 9
    3
      backend/.env.sample
  2. 47
    14
      backend/README.md
  3. 94
    35
      backend/db/data-generator/mock.js
  4. 9
    9
      backend/db/seeds/04-responses.js
  5. 12
    12
      backend/lib/auth/strategies/jwt.js
  6. 7
    9
      backend/lib/plugins/user.js
  7. 26
    17
      backend/lib/routes/filter/get.js
  8. 3
    2
      backend/lib/routes/profile/queue.js
  9. 7
    3
      backend/lib/routes/profile/score.js
  10. 2
    3
      backend/lib/routes/tag/get.js
  11. 1
    1
      backend/lib/routes/user/authentication.js
  12. 0
    67
      backend/lib/routes/user/current.js
  13. 0
    88
      backend/lib/routes/user/list-profiles.js
  14. 19
    7
      backend/lib/routes/user/login.js
  15. 59
    0
      backend/lib/routes/user/remove-session.js
  16. 15
    5
      backend/lib/routes/user/send-email.js
  17. 18
    7
      backend/lib/routes/user/token.js
  18. 10
    14
      backend/lib/routes/user/validate-session.js
  19. 13
    1
      backend/lib/routes/user/verify-session.js
  20. 32
    16
      backend/lib/services/filter.js
  21. 2
    2
      backend/lib/services/matchqueue.js
  22. 33
    23
      backend/lib/services/profile/index.js
  23. 7
    3
      backend/lib/services/profile/profiler.js
  24. 73
    57
      backend/lib/services/user.js
  25. 175
    58
      backend/package-lock.json
  26. 3
    3
      backend/package.json
  27. 74
    0
      backend/tests/user-auth-pass.spec.js
  28. 112
    0
      backend/tests/user-create-profile.spec.js
  29. 64
    0
      backend/tests/user-create-token.spec.js
  30. 111
    0
      backend/tests/user-credential.spec.js
  31. 76
    0
      backend/tests/user-remove-session.spec.js
  32. 147
    0
      backend/tests/user-send-email.spec.js
  33. 101
    0
      backend/tests/user-signup.spec.js
  34. 173
    0
      backend/tests/user-validate-session.spec.js
  35. 70
    0
      backend/tests/user-verify-session.spec.js
  36. 1
    2
      frontend/src/App.vue
  37. 3
    13
      frontend/src/components/AspectBar.vue
  38. 4
    3
      frontend/src/components/MainNav.vue
  39. 39
    13
      frontend/src/components/onboarding/Auth.vue
  40. 7
    2
      frontend/src/components/onboarding/QuestionResponse.vue
  41. 0
    1
      frontend/src/entities/profile/profile.js
  42. 5
    7
      frontend/src/entities/survey/survey.js
  43. 22
    3
      frontend/src/router/guards.js
  44. 36
    11
      frontend/src/services/auth.service.js
  45. 1
    0
      frontend/src/services/index.js
  46. 11
    2
      frontend/src/services/login.service.js
  47. 2
    2
      frontend/src/services/user.service.js
  48. 1
    0
      frontend/src/utils/index.js
  49. 3
    3
      frontend/src/utils/lang.js
  50. 1
    1
      frontend/src/utils/survey.js
  51. 21
    2
      frontend/src/views/HomeView.vue
  52. 59
    7
      frontend/src/views/LoginView.vue
  53. 22
    41
      frontend/src/views/OnboardingView.vue
  54. 118
    65
      frontend/src/views/SurveyCompleteView.vue
  55. 10
    36
      frontend/src/views/VerifyView.vue

+ 9
- 3
backend/.env.sample 查看文件

@@ -5,11 +5,12 @@
5 5
 # API host:port
6 6
 API_HOST=localhost
7 7
 API_PORT=3001
8
+APP_SECRET=somesecret
9
+APP_SESSION_SALT=a;sldkja;ldksfja;sldkj
8 10
 
9 11
 USE_LOCAL_DB=true
10 12
 DB_TYPE=mysql
11 13
 
12
-
13 14
 # Extra pepper for auth encryption
14 15
 PEPPER=kosho
15 16
 APP_SECRET=mysecret
@@ -24,7 +25,6 @@ DB_NAME=test
24 25
 DB_USER=root
25 26
 DB_ROOT_PASSWORD=root
26 27
 
27
-
28 28
 # Config for remote planet scale production dB
29 29
 PSCALE_DB_HOST=planet-scale-db
30 30
 PSCALE_DB_PORT=3306
@@ -34,5 +34,11 @@ PSCALE_DB_BRANCH=main
34 34
 PSCALE_DB_USER=myuserpleasechange
35 35
 PSCALE_DB_PASSWORD=pscale_pw_abc123efg456hij789
36 36
 
37
-# Brevo Transactional Email API key
37
+# Brevo Transactional Email API key and other related params
38 38
 BREVO_KEY=brevo_api_key
39
+# Change this to production server
40
+# BREVO_LINK=localhost:1234
41
+BREVO_LINK=link_back_to_siimee
42
+# Change this if multiple templates made via Brevo
43
+# BREVO_TEMPLATE_ID=1
44
+BREVO_TEMPLATE_ID=id_of_brevo_template

+ 47
- 14
backend/README.md 查看文件

@@ -7,9 +7,10 @@ An API for the siimee application
7 7
 You will need...
8 8
 
9 9
 ### Node.js 14+
10
-* OSX: `brew install node` using [Homebrew](http://brew.sh/)
11
-* Linux: `apt install nodejs` ([see Ubuntu/Debian specific instructions](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions)) or `pacman -S nodejs` (Arch Linux)
12
-* Windows: [Install](https://nodejs.org/en/download/)
10
+
11
+-   OSX: `brew install node` using [Homebrew](http://brew.sh/)
12
+-   Linux: `apt install nodejs` ([see Ubuntu/Debian specific instructions](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions)) or `pacman -S nodejs` (Arch Linux)
13
+-   Windows: [Install](https://nodejs.org/en/download/)
13 14
 
14 15
 ## :package: Installation
15 16
 
@@ -19,20 +20,43 @@ You will need...
19 20
 4. A test database container is provided and can be started with `docker-compose up -d`
20 21
 5. Populate some basic tables for your database using `npm run generate && npm run reseed`
21 22
 
23
+## :package: Installing Brevo
24
+
25
+This note is added in case Brevo fails to install correctly, as it requires some extra configuration:
26
+
27
+1. Should brevo fail to work upon spinning up, ensure that brevo is installed:
28
+
29
+```
30
+npm install @getbrevo/brevo --save
31
+```
32
+
33
+2. Then run 'npm install' again.
34
+3. Link the brevo package globally by running 'npm link'.
35
+4. You'll also need to link it directly within the backend directory (here):
36
+
37
+```
38
+npm link .
39
+```
40
+
41
+Transactional emails using the Brevo api should now be working.
42
+
22 43
 ## :package: Migrations
23 44
 
24 45
 We use Knex.js,which comes with a couple nice database migration and seeding tools.
25 46
 Migrating tracks changes in schema. Migration steps are contained in the `./db/migrations` folder. Seeding adds dummy data once our database tables have been created and schemas are set. Starting seed steps are contained in `./db/seeds` and use the `mock.js` file as the main dummy data source.
26 47
 
27 48
 ### Migrating
28
-* Run `npm run migrate` to run all migrations and establish the base schema
29
-* Run `npm run unmigrate` to roll back one migration
49
+
50
+-   Run `npm run migrate` to run all migrations and establish the base schema
51
+-   Run `npm run unmigrate` to roll back one migration
30 52
 
31 53
 ### Seeding
32
-* Run `npm run generate` to generate dummy data
33
-* Run `npm run seed` to seed the database with generated dummy data
54
+
55
+-   Run `npm run generate` to generate dummy data
56
+-   Run `npm run seed` to seed the database with generated dummy data
34 57
 
35 58
 ### Restarting
59
+
36 60
 Since we can't unseed the database, it's best to destroy the `dev` database and rebuild it.
37 61
 
38 62
 1. Stop the database by navigating to the project root directory `cd ../` and running `docker-compose down`
@@ -52,13 +76,22 @@ TBD
52 76
 ## :pill: Tests & Code Quality
53 77
 
54 78
 Tests are run with AVA with code coverage reporting via nyc. Look at the example test for ideas, as well as the [ava documentation](https://github.com/avajs/ava/blob/main/docs/01-writing-tests.md)
55
-* Run tests with coverage report with `npm test`
79
+
80
+-   Run tests with coverage report with `npm test`
56 81
 
57 82
 ## :heart: Built With
58 83
 
59
-* [Hapi](https://hapy.dev/) - Happy APIs
60
-* [Objection](https://vincit.github.io/objection.js/) - Light ORM
61
-* [Knex.js](https://knexjs.org/) - Query builder
62
-* [Schiwfty](https://hapipal.com/docs/schwifty) - Model layer
63
-* [Ava](https://github.com/avajs/ava) - Easy testing
64
-* [Nyc](https://github.com/istanbuljs/nyc) - Test coverage with Istanbul's CLI
84
+-   [Hapi](https://hapy.dev/) - Happy APIs
85
+-   [Objection](https://vincit.github.io/objection.js/) - Light ORM
86
+-   [Knex.js](https://knexjs.org/) - Query builder
87
+-   [Schiwfty](https://hapipal.com/docs/schwifty) - Model layer
88
+-   [Ava](https://github.com/avajs/ava) - Easy testing
89
+-   [Nyc](https://github.com/istanbuljs/nyc) - Test coverage with Istanbul's CLI
90
+
91
+## TRUNCATE TABLES IN PRODUCTION
92
+
93
+users, responses, profiles, authentication
94
+TRUNCATE TABLE users;
95
+TRUNCATE TABLE responses;
96
+TRUNCATE TABLE profiles;
97
+TRUNCATE TABLE authentication;

+ 94
- 35
backend/db/data-generator/mock.js 查看文件

@@ -425,12 +425,24 @@ module.exports = {
425 425
             tag_id: 7,
426 426
             is_deleted: false,
427 427
         },
428
+        // NOTE: profile_id 147 is chosen based off of GENERATED data,
429
+        // after running 'npm run generate', replace 147 in mock to next profile_id to be genearted
430
+        // i.e. last profile_id number + 1
431
+        // TODO: remove from mock data once bare bones matching logic can show poorly matched matches...
432
+        {
433
+            tag_association_id: 50,
434
+            profile_id: 139,
435
+            grouping_id: 2,
436
+            tag_id: 7,
437
+            is_deleted: false,
438
+        },
428 439
     ],
429 440
     response_keys: [
430 441
         {
431 442
             response_key_id: 1,
432 443
             response_key_category: 'visionary_vs_implementer',
433
-            response_key_prompt: 'Do you prefer to work with those who are driven by their Visionary insights, or those who are driven more by their Implementation?',
444
+            response_key_prompt:
445
+                'Do you prefer to work with those who are driven by their Visionary insights, or those who are driven more by their Implementation?',
434 446
             response_key_description: 'first round draft scoring question',
435 447
             aspect: 'visionary_vs_implementer',
436 448
             category: 'aspect',
@@ -440,7 +452,8 @@ module.exports = {
440 452
         {
441 453
             response_key_id: 2,
442 454
             response_key_category: 'creative_vs_methodical',
443
-            response_key_prompt: 'Have you found more success working with employees that are more Creative or those that are more Methodical?',
455
+            response_key_prompt:
456
+                'Have you found more success working with employees that are more Creative or those that are more Methodical?',
444 457
             response_key_description: 'first round draft scoring question',
445 458
             aspect: 'creative_vs_methodical',
446 459
             category: 'aspect',
@@ -450,7 +463,8 @@ module.exports = {
450 463
         {
451 464
             response_key_id: 3,
452 465
             response_key_category: 'dynamic_vs_ordered',
453
-            response_key_prompt: 'Which do you find to be the ideal working environment, one that is more Collaborative or one that is more Independent?',
466
+            response_key_prompt:
467
+                'Which do you find to be the ideal working environment, one that is more Collaborative or one that is more Independent?',
454 468
             response_key_description: 'first round draft scoring question',
455 469
             aspect: 'dynamic_vs_ordered',
456 470
             category: 'aspect',
@@ -460,7 +474,8 @@ module.exports = {
460 474
         {
461 475
             response_key_id: 4,
462 476
             response_key_category: 'precise_vs_resourceful',
463
-            response_key_prompt: 'Is the success of your team more likely if it includes individuals who are more Innovative, or those that are more Conventional when fulfilling their job duties?',
477
+            response_key_prompt:
478
+                'Is the success of your team more likely if it includes individuals who are more Innovative, or those that are more Conventional when fulfilling their job duties?',
464 479
             response_key_description: 'first round draft scoring question',
465 480
             aspect: 'precise_vs_resourceful',
466 481
             category: 'aspect',
@@ -470,7 +485,8 @@ module.exports = {
470 485
         {
471 486
             response_key_id: 5,
472 487
             response_key_category: 'big_Picture_vs_focused',
473
-            response_key_prompt: 'When fulfilling the role of the hiring leader, do you find yourself focusing more on the Big Picture or The Task At Hand?',
488
+            response_key_prompt:
489
+                'When fulfilling the role of the hiring leader, do you find yourself focusing more on the Big Picture or The Task At Hand?',
474 490
             response_key_description: 'first round draft scoring question',
475 491
             aspect: 'big_Picture_vs_focused',
476 492
             category: 'aspect',
@@ -480,7 +496,8 @@ module.exports = {
480 496
         {
481 497
             response_key_id: 6,
482 498
             response_key_category: 'guided_vs_self-managed',
483
-            response_key_prompt: 'Do you prefer to Guide your employees towards achieving the team goals, or do you prefer your employees to be Self-Managed?',
499
+            response_key_prompt:
500
+                'Do you prefer to Guide your employees towards achieving the team goals, or do you prefer your employees to be Self-Managed?',
484 501
             response_key_description: 'first round draft scoring question',
485 502
             aspect: 'guided_vs_self-managed',
486 503
             category: 'aspect',
@@ -490,7 +507,8 @@ module.exports = {
490 507
         {
491 508
             response_key_id: 7,
492 509
             response_key_category: 'profile',
493
-            response_key_prompt: 'First things first, could you provide us with your name? [break] I am called [break] when others address me.',
510
+            response_key_prompt:
511
+                'First things first, could you provide us with your name? [break] I am called [break] when others address me.',
494 512
             response_key_description: 'required for profile creation',
495 513
             aspect: null,
496 514
             category: 'input',
@@ -500,122 +518,150 @@ module.exports = {
500 518
         {
501 519
             response_key_id: 8,
502 520
             response_key_category: 'profile',
503
-            response_key_prompt: 'In order for others to reach out to you on Siimee, we will need you to provide your email address.[break]When reaching out to me, [break] is my preferred email.',
521
+            response_key_prompt:
522
+                'In order for others to reach out to you on Siimee, we will need you to provide your email address.[break]When reaching out to me, [break] is my preferred email.',
504 523
             response_key_description: 'required for profile creation',
505 524
             aspect: null,
506 525
             category: 'input',
507 526
             placeholder: 'joe@mailme.com',
508
-            invalidInputPrompt: 'It looks like that email is not valid, try en email that is formatted like so: joe@joe.com',
527
+            invalidInputPrompt:
528
+                'It looks like that email is not valid, try en email that is formatted like so: joe@joe.com',
509 529
         },
510 530
         {
511 531
             response_key_id: 9,
512 532
             response_key_category: 'profile',
513
-            response_key_prompt: 'So far so good! Next we will need you to establish a super secret password. Your password should be at least 14 characters long and have at least 2 special characters.[break]My [break] is a very secure passcode that only I will have access to!',
533
+            response_key_prompt:
534
+                'So far so good! Next we will need you to establish a super secret password. Your password should be at least 14 characters long and have at least 2 special characters.[break]My [break] is a very secure passcode that only I will have access to!',
514 535
             response_key_description: 'required for profile creation',
515 536
             aspect: null,
516 537
             category: 'input',
517 538
             placeholder: 'supersecr3tp@ssword',
518
-            invalidInputPrompt: 'That password does not fit our requirements, please follow the above instructions to generate a secure password.',
539
+            invalidInputPrompt:
540
+                'That password does not fit our requirements, please follow the above instructions to generate a secure password.',
519 541
         },
520 542
         {
521 543
             response_key_id: 10,
522 544
             response_key_category: 'profile',
523
-            response_key_prompt: 'Looking good! Doing great. The next piece of info needed is your zip code. That way we can be sure to only show you other people in your area.[break]My zip code, [break] is the general area where I wish to see results in.',
545
+            response_key_prompt:
546
+                'Looking good! Doing great. The next piece of info needed is your zip code. That way we can be sure to only show you other people in your area.[break]My zip code, [break] is the general area where I wish to see results in.',
524 547
             response_key_description: 'required for distance calculations',
525 548
             aspect: null,
526 549
             category: 'input',
527 550
             placeholder: '90012',
528
-            invalidInputPrompt: 'Oops! That is not a recognized zipcode, please enter a 5 digit zipcode like: 97869',
551
+            invalidInputPrompt:
552
+                'Oops! That is not a recognized zipcode, please enter a 5 digit zipcode like: 97869',
529 553
         },
530 554
         {
531 555
             response_key_id: 11,
532 556
             response_key_category: 'profile',
533
-            response_key_prompt: 'What are you seeking? Are you looking to find a position to be employed in, or are you looking to employ a candidate?[break] I am a [break] seeking an employer/employee.',
557
+            response_key_prompt:
558
+                'What are you seeking? Are you looking to find a position to be employed in, or are you looking to employ a candidate?[break] I am a [break] seeking an employer/employee.',
534 559
             response_key_description: 'required for profile generation',
535 560
             aspect: null,
536 561
             category: 'choice',
537 562
             placeholder: null,
538
-            invalidInputPrompt: 'In order to provide you with the best results, Siimee will need to know whether you are an employer looking to fill a position, or a candidate looking for an employment. Please take a look at our above options and choose one.',
563
+            invalidInputPrompt:
564
+                'In order to provide you with the best results, Siimee will need to know whether you are an employer looking to fill a position, or a candidate looking for an employment. Please take a look at our above options and choose one.',
539 565
         },
540 566
         {
541 567
             response_key_id: 12,
542 568
             response_key_category: 'profile',
543
-            response_key_prompt: 'Hey, you are almost done! Please provide an image of yourself so others can recognize you if you ever meet up IRL:',
569
+            response_key_prompt:
570
+                'Hey, you are almost done! Please provide an image of yourself so others can recognize you if you ever meet up IRL:',
544 571
             response_key_description: 'required for profile pictures',
545 572
             aspect: null,
546 573
             category: 'input',
547 574
             placeholder: null,
548
-            invalidInputPrompt: 'It appears you have yet to upload an image. Please provide Siimee with an image in case you want to show others what you look like.',
575
+            invalidInputPrompt:
576
+                'It appears you have yet to upload an image. Please provide Siimee with an image in case you want to show others what you look like.',
549 577
         },
550 578
         {
551 579
             response_key_id: 13,
552 580
             response_key_category: 'profile',
553
-            response_key_prompt: 'What language is your native language?[break] I consider [break] language as my native language.',
554
-            response_key_description: 'programming and spoken language preference',
581
+            response_key_prompt:
582
+                'What language is your native language?[break] I consider [break] language as my native language.',
583
+            response_key_description:
584
+                'programming and spoken language preference',
555 585
             aspect: null,
556 586
             category: 'choice',
557 587
             placeholder: null,
558
-            invalidInputPrompt: 'We try our best to provide results in the language of your choosing. ¿Prefieres ver resultados en español? Ou peut-être parlez-vous français? Or would you prefer to see results in english?',
588
+            invalidInputPrompt:
589
+                'We try our best to provide results in the language of your choosing. ¿Prefieres ver resultados en español? Ou peut-être parlez-vous français? Or would you prefer to see results in english?',
559 590
         },
560 591
         {
561 592
             response_key_id: 14,
562 593
             response_key_category: 'profile',
563
-            response_key_prompt: 'What kind of duration would you prefer? Are you looking for part-time, full-time, other?[break] Currently, I am looking for a [break] job at this time.',
564
-            response_key_description: 'duration preference for hours able to dedicate to work',
594
+            response_key_prompt:
595
+                'What kind of duration would you prefer? Are you looking for part-time, full-time, other?[break] Currently, I am looking for a [break] job at this time.',
596
+            response_key_description:
597
+                'duration preference for hours able to dedicate to work',
565 598
             aspect: null,
566 599
             category: 'choice',
567 600
             placeholder: null,
568
-            invalidInputPrompt: 'Looks like you have yet to  fill out what kind of work you are most interested in. As in, part-time, full-time. Take a look at our above options and choose whatever feels right for you right now. You can always edit them later!',
601
+            invalidInputPrompt:
602
+                'Looks like you have yet to  fill out what kind of work you are most interested in. As in, part-time, full-time. Take a look at our above options and choose whatever feels right for you right now. You can always edit them later!',
569 603
         },
570 604
         {
571 605
             response_key_id: 15,
572 606
             response_key_category: 'profile',
573
-            response_key_prompt: 'Would you prefer remote, hybrid, in-person work?[break] Personally I would prefer a [break] job right now. It is just what works best for me.',
574
-            response_key_description: 'location preference for where work happens',
607
+            response_key_prompt:
608
+                'Would you prefer remote, hybrid, in-person work?[break] Personally I would prefer a [break] job right now. It is just what works best for me.',
609
+            response_key_description:
610
+                'location preference for where work happens',
575 611
             aspect: null,
576 612
             category: 'choice',
577 613
             placeholder: null,
578
-            invalidInputPrompt: 'Hold up! So sorry to put a pause here, but it looks like you have not chosen whether to work remotely or in person. No worries, if you are unsure, just choose the flexible option.',
614
+            invalidInputPrompt:
615
+                'Hold up! So sorry to put a pause here, but it looks like you have not chosen whether to work remotely or in person. No worries, if you are unsure, just choose the flexible option.',
579 616
         },
580 617
         {
581 618
             response_key_id: 16,
582 619
             response_key_category: 'profile',
583
-            response_key_prompt: 'Please provide us with a short blurb about yourself. What is your backstory?[break] My origin story starts like this:[break]',
620
+            response_key_prompt:
621
+                'Please provide us with a short blurb about yourself. What is your backstory?[break] My origin story starts like this:[break]',
584 622
             response_key_description: 'required for profile description',
585 623
             aspect: null,
586 624
             category: 'input',
587 625
             placeholder: 'my backstory starts long long ago...',
588
-            invalidInputPrompt: 'Whoa! Cool story. Unfortunately your backstory is either too long or too short. Please tell us a bit about yourself between 1 and 100 characters.',
626
+            invalidInputPrompt:
627
+                'Whoa! Cool story. Unfortunately your backstory is either too long or too short. Please tell us a bit about yourself between 1 and 100 characters.',
589 628
         },
590 629
         {
591 630
             response_key_id: 17,
592 631
             response_key_category: 'profile',
593
-            response_key_prompt: 'How soon do you need the position filled or you need to be employed? [break]I am currently [break] when it comes to employment opportunities right now.',
632
+            response_key_prompt:
633
+                'How soon do you need the position filled or you need to be employed? [break]I am currently [break] when it comes to employment opportunities right now.',
594 634
             response_key_description: 'urgency for when work is required',
595 635
             aspect: null,
596 636
             category: 'choice',
597 637
             placeholder: null,
598
-            invalidInputPrompt: 'Looks like you left this field blank. Take a look at our provided options and tell us when you would like be employed.',
638
+            invalidInputPrompt:
639
+                'Looks like you left this field blank. Take a look at our provided options and tell us when you would like be employed.',
599 640
         },
600 641
         {
601 642
             response_key_id: 18,
602 643
             response_key_category: 'profile',
603
-            response_key_prompt: 'When others refer to you, what pronouns do you prefer they use?[break]I prefer to be called [break] when others refer to me.',
644
+            response_key_prompt:
645
+                'When others refer to you, what pronouns do you prefer they use?[break]I prefer to be called [break] when others refer to me.',
604 646
             response_key_description: 'required for profile pronouns',
605 647
             aspect: null,
606 648
             category: 'choice',
607 649
             placeholder: null,
608
-            invalidInputPrompt: 'Ensuring that others on our platform are aware of what your preferred pronouns are is important to us. Please choose from one of the above options.',
650
+            invalidInputPrompt:
651
+                'Ensuring that others on our platform are aware of what your preferred pronouns are is important to us. Please choose from one of the above options.',
609 652
         },
610 653
         {
611 654
             response_key_id: 19,
612 655
             response_key_category: 'profile',
613
-            response_key_prompt: 'What distance from your home are you looking to work in?[break] Preferably, I would like to work [break] from my place of residence.',
614
-            response_key_description: 'preference for commuting distance cutoff',
656
+            response_key_prompt:
657
+                'What distance from your home are you looking to work in?[break] Preferably, I would like to work [break] from my place of residence.',
658
+            response_key_description:
659
+                'preference for commuting distance cutoff',
615 660
             aspect: null,
616 661
             category: 'input',
617 662
             placeholder: '5 mi',
618
-            invalidInputPrompt: 'Whoa! You either left this field blank or tried to input an astronomically large distance you would like to see results from. Please input a distance you would like to see results in.',
663
+            invalidInputPrompt:
664
+                'Whoa! You either left this field blank or tried to input an astronomically large distance you would like to see results from. Please input a distance you would like to see results in.',
619 665
         },
620 666
     ],
621 667
     responses: [],
@@ -636,6 +682,14 @@ module.exports = {
636 682
             can_edit: false,
637 683
             is_active: true,
638 684
         },
685
+        {
686
+            membership_id: 4,
687
+            profile_id: 46,
688
+            grouping_id: 2,
689
+            membership_type: 'participant',
690
+            can_edit: false,
691
+            is_active: true,
692
+        },
639 693
     ],
640 694
     groupings: [
641 695
         {
@@ -643,6 +697,11 @@ module.exports = {
643 697
             grouping_name: '1663285820.067_41_46',
644 698
             grouping_type: 'match',
645 699
         },
700
+        {
701
+            grouping_id: 2,
702
+            grouping_name: '1663285820.067_147_46',
703
+            grouping_type: 'match',
704
+        },
646 705
     ],
647 706
     messages: [],
648 707
     match_queues: [

+ 9
- 9
backend/db/seeds/04-responses.js 查看文件

@@ -12,24 +12,24 @@ for (let name of fileNames) {
12 12
         responses = [...responses, ...data.responses]
13 13
     }
14 14
 }
15
-
16 15
 /**
17 16
  * Prevent seeding responses for
18 17
  * profile ids so we can test oboarding
19 18
  */
20
-responses = dataSort(responses, 'response_id').filter(
21
-    response => !ignore.includes(response.profile_id),
22
-)
19
+// responses = dataSort(responses, 'response_id').filter(
20
+// response => !ignore.includes(response.profile_id),
21
+// )
23 22
 
24 23
 exports.seed = async knex => {
25 24
     await knex('responses').del()
26 25
     let responsesToPush = []
27
-    let len = responses.length
26
+    const len = responses.length
28 27
     for (let i = 1; i <= len; i += 1) {
29 28
         responsesToPush.push(responses.shift())
30
-        if (i % batchSize === 0 || i > responses.length) {
31
-            // await knex('responses').insert(responsesToPush)
32
-            responsesToPush = []
33
-        }
29
+        // if (i % batchSize === 0 || i > responses.length) {
30
+        // await knex('responses').insert(responsesToPush)
31
+        // responsesToPush = []
32
+        // }
34 33
     }
34
+    await knex('responses').insert(responsesToPush)
35 35
 }

+ 12
- 12
backend/lib/auth/strategies/jwt.js 查看文件

@@ -36,32 +36,32 @@ module.exports = options => {
36 36
         },
37 37
         // TODO: Naming conventions need to be reversed again??
38 38
         validate: async (decoded, request, h) => {
39
-            const accessTokenFromHeaders = request.headers.authorization
40
-            const hashedAccessTokenFromHeaders = await hashToken(
41
-                accessTokenFromHeaders,
39
+            const sessionTokenFromHeaders = request.headers.authorization
40
+            const hashedSessionTokenFromHeaders = await hashToken(
41
+                sessionTokenFromHeaders,
42 42
             )
43 43
             const activeSession =
44
-                request.server.app.activeSessions[hashedAccessTokenFromHeaders]
44
+                request.server.app.activeSessions[hashedSessionTokenFromHeaders]
45 45
             if (!activeSession)
46 46
                 throw new Error(
47
-                    `No session found for ${hashedAccessTokenFromHeaders}`,
47
+                    `No session found for ${hashedSessionTokenFromHeaders}`,
48 48
                 )
49 49
 
50
-            const accessToken = activeSession.accessToken
51 50
             const sessionToken = activeSession.sessionToken
52
-            const validatedAccessToken = validateToken(accessToken)
51
+            const accessToken = activeSession.accessToken
53 52
             const validatedSessionToken = validateToken(sessionToken)
54
-            if (!validatedSessionToken.payload) {
55
-                console.log('sessionToken no longer valid, reissuing... ')
56
-                activeSession.sessionToken = createToken(
57
-                    { payload: validatedAccessToken.payload },
53
+            const validatedAccessToken = validateToken(accessToken)
54
+            if (!validatedAccessToken.payload) {
55
+                console.log('accessToken no longer valid, reissuing... ')
56
+                activeSession.accessToken = createToken(
57
+                    { payload: validatedSessionToken.payload },
58 58
                     // NOTE: Expiration of new sessionToken set for 200 seconds (testing)
59 59
                     100,
60 60
                 )
61 61
             }
62 62
             try {
63 63
                 const validatedJwt = JWT.verify(
64
-                    accessToken,
64
+                    sessionToken,
65 65
                     process.env.APP_SECRET,
66 66
                 )
67 67
                 return { isValid: true, credentials: validatedJwt.email }

+ 7
- 9
backend/lib/plugins/user.js 查看文件

@@ -7,15 +7,14 @@ const JwtStrategy = require('../auth/strategies/jwt')
7 7
 const UserModel = require('../models/user')
8 8
 const AuthModel = require('../models/authentication')
9 9
 
10
-const UserCurrentRoute = require('../routes/user/current')
11 10
 const UserProfileCreateRoute = require('../routes/user/create-profile')
12
-const UserProfilesListRoute = require('../routes/user/list-profiles')
13 11
 const UserLoginRoute = require('../routes/user/login')
14 12
 const UserSignupRoute = require('../routes/user/signup')
15
-const UserEmailRoute = require('../routes/user/email.js')
16
-const UserVerifyActiveRoute = require('../routes/user/verifyactivesession.js')
17
-const UserGetAccessRoute = require('../routes/user/getaccess.js')
18
-const UserValidateSessionRoute = require('../routes/user/validatesession.js')
13
+const UserEmailRoute = require('../routes/user/send-email.js')
14
+const UserVerifyActiveRoute = require('../routes/user/verify-session.js')
15
+const UserCreateTokenRoute = require('../routes/user/token.js')
16
+const UserValidateSessionRoute = require('../routes/user/validate-session.js')
17
+const UserRemoveSessionRoute = require('../routes/user/remove-session.js')
19 18
 const UserPassword = require('../routes/user/authentication')
20 19
 
21 20
 const UserService = require('../services/user')
@@ -48,15 +47,14 @@ module.exports = {
48 47
         server.registerService(DisplayService)
49 48
         server.registerService(HealthService)
50 49
 
51
-        await server.route(UserCurrentRoute)
52 50
         await server.route(UserLoginRoute)
53 51
         await server.route(UserSignupRoute)
54 52
         await server.route(UserProfileCreateRoute)
55
-        await server.route(UserProfilesListRoute)
56 53
         await server.route(UserEmailRoute)
57 54
         await server.route(UserVerifyActiveRoute)
58
-        await server.route(UserGetAccessRoute)
55
+        await server.route(UserCreateTokenRoute)
59 56
         await server.route(UserValidateSessionRoute)
57
+        await server.route(UserRemoveSessionRoute)
60 58
         await server.route(UserPassword)
61 59
     },
62 60
 }

+ 26
- 17
backend/lib/routes/filter/get.js 查看文件

@@ -1,5 +1,7 @@
1 1
 'use strict'
2 2
 
3
+// NOTE: Current Implementation does not require this route
4
+// (see Auth.vue in components/onboarding and queue route with filter.js)
3 5
 const Joi = require('joi')
4 6
 const apiSchema = require('../../schemas/api')
5 7
 const errorSchema = require('../../schemas/errors')
@@ -10,27 +12,27 @@ const pluginConfig = {
10 12
     handlerType: 'filter',
11 13
     docs: {
12 14
         description: 'Filter match pool',
13
-        notes: 'Returns filtered subset of match pool'
14
-    }
15
+        notes: 'Returns filtered subset of match pool',
16
+    },
15 17
 }
16 18
 
17 19
 const validators = {
18 20
     query: Joi.object({
19 21
         match_pool: Joi.array().items(profileSchema.single),
20 22
         distance: Joi.string(),
21
-        presence: Joi.string()
22
-    })
23
+        presence: Joi.string(),
24
+    }),
23 25
 }
24 26
 
25 27
 const responseSchemas = {
26 28
     filteredMatchPool: filterSchema.matchPool, // array of profiles
27
-    error: errorSchema.single
29
+    error: errorSchema.single,
28 30
 }
29 31
 
30 32
 module.exports = {
31 33
     method: 'GET',
32 34
     path: '/',
33
-    options:{
35
+    options: {
34 36
         ...pluginConfig.docs,
35 37
         tags: ['api'],
36 38
         auth: false,
@@ -38,28 +40,35 @@ module.exports = {
38 40
         handler: async function (request, h) {
39 41
             const { filterService } = request.server.services()
40 42
             let matchPool = request.query.match_pool
41
-            matchPool = filterService.byDistance(matchPool, request.query.distance)
42
-            matchPool = filterService.byPresence(matchPool, request.query.presence)
43
-      
43
+            matchPool = filterService.byDistance(
44
+                matchPool,
45
+                request.query.distance,
46
+            )
47
+            matchPool = filterService.byPresence(
48
+                matchPool,
49
+                request.query.presence,
50
+            )
44 51
             try {
45
-                return h.response(({
46
-                    ok:true,
47
-                    handler: pluginConfig.handlerType,
48
-                    data: matchPool
49
-                })).code(200)
52
+                return h
53
+                    .response({
54
+                        ok: true,
55
+                        handler: pluginConfig.handlerType,
56
+                        data: matchPool,
57
+                    })
58
+                    .code(200)
50 59
             } catch (err) {
51 60
                 return h
52 61
                     .response({
53 62
                         ok: false,
54 63
                         handler: pluginConfig.handlerType,
55
-                        data: {error: `${err}`}
64
+                        data: { error: `${err}` },
56 65
                     })
57 66
                     .code(409)
58 67
             }
59 68
         },
60 69
         validate: {
61 70
             ...validators,
62
-            failAction: 'log'
71
+            failAction: 'log',
63 72
         },
64 73
 
65 74
         response: {
@@ -77,4 +86,4 @@ module.exports = {
77 86
             },
78 87
         },
79 88
     },
80
-}
89
+}

+ 3
- 2
backend/lib/routes/profile/queue.js 查看文件

@@ -4,7 +4,6 @@ const Joi = require('joi')
4 4
 const apiSchema = require('../../schemas/api')
5 5
 const errorSchema = require('../../schemas/errors')
6 6
 const profileSchema = require('../../schemas/profiles')
7
-const params = require('../../schemas/params')
8 7
 
9 8
 const pluginConfig = {
10 9
     handlerType: 'profile',
@@ -22,7 +21,9 @@ const responseSchemas = {
22 21
 }
23 22
 
24 23
 const validators = {
25
-    params: params.profileId,
24
+    params: Joi.object({
25
+        profile_id: Joi.string(),
26
+    }),
26 27
     query: Joi.object({
27 28
         include_profile: Joi.bool(),
28 29
         limit: Joi.number(),

+ 7
- 3
backend/lib/routes/profile/score.js 查看文件

@@ -53,10 +53,14 @@ module.exports = {
53 53
             const distanceUnit = request.query.unit
54 54
                 ? request.query.unit
55 55
                 : 'mile'
56
-            const duration = request.query.duration
57
-            const presence = request.query.presence
56
+            const duration = request.query.duration.includes('-')
57
+                ? request.query.duration.split('-')[0]
58
+                : request.query.duration
59
+            const presence =
60
+                request.query.presence === 'in_person'
61
+                    ? 'onsite'
62
+                    : request.query.presence
58 63
             const certifications = request.query.certifications
59
-
60 64
             const scoredProfiles = await profileService.scoreProfilesFor(
61 65
                 profileId,
62 66
                 maxDistanceMiles,

+ 2
- 3
backend/lib/routes/tag/get.js 查看文件

@@ -2,7 +2,6 @@
2 2
 
3 3
 const apiSchema = require('../../schemas/api')
4 4
 const errorSchema = require('../../schemas/errors')
5
-const params = require('../../schemas/params')
6 5
 const Joi = require('joi')
7 6
 
8 7
 const pluginConfig = {
@@ -20,8 +19,8 @@ const responseSchemas = {
20 19
 
21 20
 const validators = {
22 21
     params: Joi.object({
23
-        profile_id: params.profileId,
24
-        grouping_id: params.groupingId,
22
+        profile_id: Joi.string(),
23
+        grouping_id: Joi.string(),
25 24
     }),
26 25
     query: Joi.object({ category: Joi.string() }),
27 26
 }

+ 1
- 1
backend/lib/routes/user/authentication.js 查看文件

@@ -16,7 +16,7 @@ const pluginConfig = {
16 16
 /** Validator functions by request method */
17 17
 const validators = {
18 18
     /** Validate the route params (/active/{thing}) */
19
-    params: params.userEmail
19
+    params: params.userEmail,
20 20
 }
21 21
 
22 22
 module.exports = {

+ 0
- 67
backend/lib/routes/user/current.js 查看文件

@@ -1,67 +0,0 @@
1
-'use strict'
2
-
3
-const Joi = require('joi')
4
-const params = require('../../schemas/params')
5
-
6
-const pluginConfig = {
7
-    handlerType: 'user',
8
-    docs: {
9
-        get: {
10
-            description: 'Get user',
11
-            notes: 'Returns a user item by the id passed in the path',
12
-        },
13
-    },
14
-}
15
-
16
-/** Validator functions by request method */
17
-const validators = {
18
-    get: {
19
-        params: params.userName,
20
-    },
21
-}
22
-
23
-module.exports = {
24
-    method: 'get',
25
-    path: '/{name}',
26
-    options: {
27
-        ...pluginConfig.docs.get,
28
-        tags: ['api'],
29
-        auth: 'default_jwt',
30
-        handler: async function (request, h) {
31
-            try {
32
-                const auth = {
33
-                    credentials: request.auth.credentials,
34
-                    token: request.auth.artifacts.token,
35
-                }
36
-
37
-                // /** Get the data for your endpoint */
38
-                // const { User } = request.models()
39
-                // const all = await User.query()
40
-
41
-                const { displayService } = request.services()
42
-                const user = displayService.user(auth.credentials, auth.token)
43
-
44
-                return {
45
-                    ok: true,
46
-                    handler: pluginConfig.handlerType,
47
-                    data: { name: request.params.name },
48
-                }
49
-            } catch (err) {
50
-                return {
51
-                    ok: false,
52
-                    handler: pluginConfig.handlerType,
53
-                    data: { error: err },
54
-                }
55
-            }
56
-        },
57
-        validate: validators.get,
58
-        response: {
59
-            schema: Joi.object({
60
-                ok: Joi.bool(),
61
-                handler: Joi.string(),
62
-                data: validators.get.params,
63
-            }).label('user_name_res'),
64
-            failAction: 'log',
65
-        },
66
-    },
67
-}

+ 0
- 88
backend/lib/routes/user/list-profiles.js 查看文件

@@ -1,88 +0,0 @@
1
-'use strict'
2
-
3
-const Joi = require('joi')
4
-const errorSchema = require('../../schemas/errors')
5
-const profileSchema = require('../../schemas/profiles')
6
-const params = require('../../schemas/params')
7
-
8
-const pluginConfig = {
9
-    handlerType: 'user',
10
-    docs: {
11
-        description: 'profiles',
12
-        notes: 'A list of profiles associated with this user',
13
-    },
14
-}
15
-
16
-const validators = {
17
-    /** Validate the header (cookie check) */
18
-    // headers: true,
19
-
20
-    /** Validate the route params (/active/{thing}) */
21
-    params: params.userId,
22
-
23
-    /** Validate the route query (/active/{thing}?limit=10&offset=10) */
24
-    // query: true,
25
-    /** Validate the incoming payload (POST method) */
26
-    // payload: true,
27
-}
28
-
29
-const responseSchemas = {
30
-    profilesList: profileSchema.list,
31
-    error: errorSchema.single,
32
-}
33
-
34
-module.exports = {
35
-    method: 'GET',
36
-    path: '/{user_id}/profiles',
37
-    options: {
38
-        ...pluginConfig.docs,
39
-        tags: ['api'],
40
-        auth: 'default_jwt',
41
-        cors: true,
42
-        handler: async function (request, h) {
43
-            const { userService, profileService } = request.server.services()
44
-            const userId = request.params.user_id
45
-            const user = await userService.findById(userId)
46
-            const type = user.is_poster == 1 ? 'poster' : 'seeker'
47
-            const profiles = await profileService.getCompleteProfilesFor(
48
-                userId,
49
-                type,
50
-            )
51
-            try {
52
-                return {
53
-                    ok: true,
54
-                    handler: pluginConfig.handlerType,
55
-                    data: profiles,
56
-                }
57
-            } catch (err) {
58
-                return {
59
-                    ok: false,
60
-                    handler: pluginConfig.handlerType,
61
-                    data: { error: `${err}` },
62
-                }
63
-            }
64
-        },
65
-
66
-        /** Validate based on validators object */
67
-        validate: {
68
-            ...validators,
69
-            failAction: 'log',
70
-        },
71
-
72
-        /** Validate the server response */
73
-        response: {
74
-            status: {
75
-                200: Joi.object({
76
-                    ok: Joi.bool(),
77
-                    handler: Joi.string(),
78
-                    data: responseSchemas.profilesList,
79
-                }).label('list_profiles_res'),
80
-                500: Joi.object({
81
-                    ok: Joi.bool(),
82
-                    handler: Joi.string(),
83
-                    data: responseSchemas.error,
84
-                }).label('error_single_res'),
85
-            },
86
-        },
87
-    },
88
-}

+ 19
- 7
backend/lib/routes/user/login.js 查看文件

@@ -19,7 +19,6 @@ const validators = {
19 19
             user_email: Joi.string(),
20 20
             password: Joi.string(),
21 21
         }),
22
-        
23 22
     },
24 23
     user: userSchema.single,
25 24
     error: errorSchema.single,
@@ -32,10 +31,10 @@ module.exports = {
32 31
         ...pluginConfig.docs,
33 32
         tags: ['api'],
34 33
         auth: false,
34
+        cors: true,
35 35
         handler: async function (request, h) {
36 36
             try {
37
-                const { userService } = request.services()
38
-
37
+                const { userService } = request.server.services()
39 38
                 const res = request.payload
40 39
 
41 40
                 // Callback to use as transaction
@@ -50,13 +49,21 @@ module.exports = {
50 49
                 }
51 50
 
52 51
                 // Bound context from your plugin server declaration
53
-                const user = await h.context.transaction(login)
54
-                const token = userService.createToken(user)
52
+                await h.context.transaction(login)
53
+
54
+                // Uses Same Logic Behind Initial Sign Up,
55
+                // passing expected credentials to be used for logging in
56
+                const { userCredentials, token } =
57
+                    await userService.makeUserCredentials(res.user_email)
55 58
 
56 59
                 return {
57 60
                     ok: true,
58 61
                     handler: pluginConfig.handlerType,
59
-                    data: { user_email: user.user_email, jwtToken: token },
62
+                    data: {
63
+                        user_email: userCredentials.email,
64
+                        jwt: token,
65
+                        answered: userCredentials,
66
+                    },
60 67
                 }
61 68
             } catch (err) {
62 69
                 console.error(err)
@@ -75,7 +82,12 @@ module.exports = {
75 82
                     handler: Joi.string(),
76 83
                     data: Joi.object({
77 84
                         user_email: Joi.string(),
78
-                        jwtToken: Joi.string(),
85
+                        jwt: Joi.string(),
86
+                        answered: Joi.object({
87
+                            email: Joi.string(),
88
+                            name: Joi.string(),
89
+                            seeking: Joi.string(),
90
+                        }),
79 91
                     }),
80 92
                 }).label('login_res'),
81 93
                 409: Joi.object({

+ 59
- 0
backend/lib/routes/user/remove-session.js 查看文件

@@ -0,0 +1,59 @@
1
+'use strict'
2
+
3
+const { plugin } = require('@hapi/inert')
4
+const Joi = require('joi')
5
+
6
+const pluginConfig = {
7
+    handlerType: 'jwt',
8
+    docs: {
9
+        get: {
10
+            description: 'removes sessionToken from activeSessions upon logout',
11
+            notes: 'On logout, activeSessions no longer holds onto user credentials',
12
+        },
13
+    },
14
+}
15
+
16
+module.exports = {
17
+    method: 'POST',
18
+    path: '/remove-session',
19
+    options: {
20
+        ...pluginConfig.docs.get,
21
+        tags: ['api'],
22
+        auth: false,
23
+        cors: {
24
+            headers: ['Authorization', 'Content-Type'],
25
+            exposedHeaders: ['Authorization', 'Access-Control-Expose-Headers'],
26
+        },
27
+        handler: async function (request, h) {
28
+            const hashedSessionToken = request.payload
29
+            const { userService } = request.server.services()
30
+            try {
31
+                await userService.removeSession(hashedSessionToken)
32
+                return {
33
+                    ok: true,
34
+                    handler: pluginConfig.handlerType,
35
+                    data: {
36
+                        sessionTokenIsRemoved: true,
37
+                    },
38
+                }
39
+            } catch (err) {
40
+                return {
41
+                    ok: false,
42
+                    handler: pluginConfig.handlerType,
43
+                    data: { error: err.message },
44
+                }
45
+            }
46
+        },
47
+        validate: {
48
+            failAction: 'log',
49
+        },
50
+        response: {
51
+            schema: Joi.object({
52
+                ok: Joi.bool(),
53
+                handler: Joi.string(),
54
+                data: Joi.object(),
55
+            }).label('validate_session_res'),
56
+            failAction: 'log',
57
+        },
58
+    },
59
+}

backend/lib/routes/user/email.js → backend/lib/routes/user/send-email.js 查看文件

@@ -12,9 +12,18 @@ const pluginConfig = {
12 12
     },
13 13
 }
14 14
 
15
+const validators = {
16
+    payload: Joi.object({
17
+        email: Joi.string(),
18
+        name: Joi.string(),
19
+        seeking: Joi.string(),
20
+        sessionToken: Joi.string(),
21
+    }),
22
+}
23
+
15 24
 module.exports = {
16 25
     method: 'POST',
17
-    path: '/sendemail/',
26
+    path: '/send-email/',
18 27
     options: {
19 28
         ...pluginConfig.docs.get,
20 29
         tags: ['api'],
@@ -25,7 +34,7 @@ module.exports = {
25 34
             const userCredentials = request.payload
26 35
             try {
27 36
                 const emailSent = await userService.emailSent(userCredentials)
28
-                const hashedAccessToken = Object.keys(
37
+                const hashedSessionToken = Object.keys(
29 38
                     userService.activeSessions,
30 39
                 ).find(hashedToken => {
31 40
                     return (
@@ -35,15 +44,15 @@ module.exports = {
35 44
                 })
36 45
                 // Registers the activeSessions object for use by jwt auth strategy
37 46
                 request.server.app.activeSessions = userService.activeSessions
38
-                if (!hashedAccessToken.length) {
39
-                    throw Error('hashedAccessToken not Found!!')
47
+                if (!hashedSessionToken?.length) {
48
+                    throw Error('hashedSessionToken not Found!!')
40 49
                 }
41 50
                 return {
42 51
                     ok: true,
43 52
                     handler: pluginConfig.handlerType,
44 53
                     data: {
45 54
                         emailSentSuccessfully: emailSent.wasSuccessfull,
46
-                        hashedAccessToken: hashedAccessToken,
55
+                        hashedSessionToken,
47 56
                     },
48 57
                 }
49 58
             } catch (err) {
@@ -58,6 +67,7 @@ module.exports = {
58 67
             }
59 68
         },
60 69
         validate: {
70
+            ...validators,
61 71
             failAction: 'log',
62 72
         },
63 73
         response: {

backend/lib/routes/user/getaccess.js → backend/lib/routes/user/token.js 查看文件

@@ -6,15 +6,25 @@ const pluginConfig = {
6 6
     handlerType: 'authentication',
7 7
     docs: {
8 8
         get: {
9
-            description: 'gets session token for authentication',
10
-            notes: 'Gets session token for authentication',
9
+            description: 'creates session token for authentication',
10
+            notes: 'Creates session token for authentication',
11 11
         },
12 12
     },
13 13
 }
14 14
 
15
+const validators = {
16
+    payload: Joi.object({
17
+        payload: Joi.object({
18
+            email: Joi.string(),
19
+            name: Joi.string(),
20
+            seeking: Joi.string(),
21
+        }),
22
+    }),
23
+}
24
+
15 25
 module.exports = {
16 26
     method: 'POST',
17
-    path: '/getaccess',
27
+    path: '/token',
18 28
     options: {
19 29
         ...pluginConfig.docs.get,
20 30
         tags: ['api'],
@@ -26,15 +36,15 @@ module.exports = {
26 36
         handler: async function (request, h) {
27 37
             const { userService } = request.server.services()
28 38
             const res = request.payload
29
-            // NOTE: Access Token set for 5 minutes expiration (default)
30
-            const accessToken = await userService.createToken(res, 600)
39
+            // NOTE: Session Token set for 5 minutes expiration (default)
40
+            const sessionToken = await userService.createToken(res, 600)
31 41
             try {
32 42
                 const response = h.response({
33 43
                     ok: true,
34 44
                     handler: pluginConfig.handlerType,
35
-                    data: accessToken,
45
+                    data: sessionToken,
36 46
                 })
37
-                response.header('Authorization', accessToken)
47
+                response.header('Authorization', sessionToken)
38 48
                 return response
39 49
             } catch (err) {
40 50
                 return {
@@ -47,6 +57,7 @@ module.exports = {
47 57
             }
48 58
         },
49 59
         validate: {
60
+            ...validators,
50 61
             failAction: 'log',
51 62
         },
52 63
         response: {

backend/lib/routes/user/validatesession.js → backend/lib/routes/user/validate-session.js 查看文件

@@ -1,6 +1,5 @@
1 1
 'use strict'
2 2
 
3
-const { plugin } = require('@hapi/inert')
4 3
 const Joi = require('joi')
5 4
 
6 5
 const pluginConfig = {
@@ -8,14 +7,18 @@ const pluginConfig = {
8 7
     docs: {
9 8
         get: {
10 9
             description: 'validates session token for each step of survey',
11
-            notes: 'validates session token for each step of survey',
10
+            notes: 'Validates session token for each step of survey',
12 11
         },
13 12
     },
14 13
 }
15 14
 
15
+const validators = {
16
+    payload: Joi.string(),
17
+}
18
+
16 19
 module.exports = {
17 20
     method: 'POST',
18
-    path: '/validatesession',
21
+    path: '/validate-session',
19 22
     options: {
20 23
         ...pluginConfig.docs.get,
21 24
         tags: ['api'],
@@ -25,35 +28,27 @@ module.exports = {
25 28
             exposedHeaders: ['Authorization', 'Access-Control-Expose-Headers'],
26 29
         },
27 30
         handler: async function (request, h) {
28
-            const hashedAccessToken = request.payload
31
+            const hashedSessionToken = request.payload
29 32
             const { userService, profileService } = request.server.services()
30 33
             try {
31 34
                 const validatedSessionToken =
32
-                    userService.validateSession(hashedAccessToken)
35
+                    userService.validateSession(hashedSessionToken)
33 36
                 const user = await userService.findByUserEmail(
34 37
                     validatedSessionToken.email,
35 38
                 )
36
-                const type = user.is_poster == 1 ? 'poster' : 'seeker'
39
+                const type = user.is_poster === 1 ? 'poster' : 'seeker'
37 40
                 const profiles = await profileService.getCompleteProfilesFor(
38 41
                     user.user_id,
39 42
                     type,
40 43
                 )
41 44
                 // TODO: handle user with multiple profiles...
42 45
                 const profileId = profiles[0].profile_id
43
-                const responses = []
44
-                profiles[0].responses.forEach(response => {
45
-                    responses.push({
46
-                        response_key_id: response.response_key_id,
47
-                        val: response.val,
48
-                    })
49
-                })
50 46
                 return {
51 47
                     ok: true,
52 48
                     handler: pluginConfig.handlerType,
53 49
                     data: {
54 50
                         ...validatedSessionToken,
55 51
                         profileId: profileId,
56
-                        responses: responses,
57 52
                     },
58 53
                 }
59 54
             } catch (err) {
@@ -65,6 +60,7 @@ module.exports = {
65 60
             }
66 61
         },
67 62
         validate: {
63
+            ...validators,
68 64
             failAction: 'log',
69 65
         },
70 66
         response: {

backend/lib/routes/user/verifyactivesession.js → backend/lib/routes/user/verify-session.js 查看文件

@@ -12,6 +12,12 @@ const pluginConfig = {
12 12
     },
13 13
 }
14 14
 
15
+const validators = {
16
+    params: Joi.object({
17
+        hashedSessionToken: Joi.string(),
18
+    }),
19
+}
20
+
15 21
 module.exports = {
16 22
     method: 'GET',
17 23
     path: '/verify/{hashedSessionToken}',
@@ -29,7 +35,7 @@ module.exports = {
29 35
                 ).find(hashedToken => {
30 36
                     return hashedToken === hash
31 37
                 })
32
-                if (!hashToMatch.length) {
38
+                if (!hashToMatch?.length) {
33 39
                     throw Error('hashToMatch Not Found!')
34 40
                 }
35 41
                 const now = Date.now()
@@ -45,6 +51,11 @@ module.exports = {
45 51
                 if (!hashToMatch) {
46 52
                     throw new Error('no record of email in cache')
47 53
                 }
54
+                // NOTE: When user responds to email,
55
+                // boolean value is set to true, allowing user back into the survey
56
+                userService.activeSessions[
57
+                    hashToMatch
58
+                ].emailWasRespondedTo = true
48 59
                 return {
49 60
                     ok: true,
50 61
                     handler: pluginConfig.handlerType,
@@ -64,6 +75,7 @@ module.exports = {
64 75
             }
65 76
         },
66 77
         validate: {
78
+            ...validators,
67 79
             failAction: 'log',
68 80
         },
69 81
         response: {

+ 32
- 16
backend/lib/services/filter.js 查看文件

@@ -29,24 +29,40 @@ module.exports = class FilterService extends Schmervice.Service {
29 29
         })
30 30
     }
31 31
 
32
-    byDuration(profileList, duration) {
33
-        return profileList.filter(profile => {
34
-            // TODO find duration
35
-            return profile.duration === duration
36
-        })
37
-    }
38
-
39 32
     byPresence(profileList, presence) {
40
-        return profileList.filter(profile => {
41
-            // TODO find presence
42
-            return profile.presence === presence
43
-        })
33
+        const matchingProfiles = []
34
+        for (const profile of profileList) {
35
+            for (const response of profile.responses) {
36
+                if (
37
+                    response.response_key_id === 15 &&
38
+                    response.val === presence
39
+                ) {
40
+                    matchingProfiles.push(profile)
41
+                }
42
+            }
43
+        }
44
+        return matchingProfiles
44 45
     }
45 46
 
46
-    byCertifications(profileList, certifications) {
47
-        return profileList.filter(profile => {
48
-            // TODO find certifications
49
-            return profile.certifications === certifications
50
-        })
47
+    byDuration(profileList, duration) {
48
+        const matchingProfiles = []
49
+        for (const profile of profileList) {
50
+            for (const response of profile.responses) {
51
+                if (
52
+                    response.response_key_id === 14 &&
53
+                    response.val === duration
54
+                ) {
55
+                    matchingProfiles.push(profile)
56
+                }
57
+            }
58
+        }
59
+        return matchingProfiles
51 60
     }
61
+
62
+    // TODO: Implement filtering by matching certification
63
+    // byCertifications(profileList, certifications) {
64
+    // return profileList.filter(profile => {
65
+    // return profile.certifications === certifications
66
+    // })
67
+    // }
52 68
 }

+ 2
- 2
backend/lib/services/matchqueue.js 查看文件

@@ -9,9 +9,9 @@ module.exports = class MatchQueueService extends Schmervice.Service {
9 9
      * @param {number} profileId
10 10
      * @returns {array} MatchQueue
11 11
      */
12
-    async getQueue(profileId, limit, offset=0) {
12
+    async getQueue(profileId, limit, offset = 0) {
13 13
         const { MatchQueue } = this.server.models()
14
-        if(typeof limit==='undefined') {
14
+        if (typeof limit === 'undefined') {
15 15
             return await MatchQueue.query()
16 16
                 .where('profile_id', profileId)
17 17
                 .where('is_deleted', 0)

+ 33
- 23
backend/lib/services/profile/index.js 查看文件

@@ -4,7 +4,8 @@ const config = require('../../../db/data-generator/config.json')
4 4
 const profiler = require('./profiler')
5 5
 const scoring = require('./scorer')
6 6
 const zipcoder = require('./zipcoder')
7
-const filter = require('../filter')
7
+const Filter = require('../filter')
8
+const filter = new Filter()
8 9
 
9 10
 module.exports = class ProfileService extends Schmervice.Service {
10 11
     constructor(...args) {
@@ -28,10 +29,12 @@ module.exports = class ProfileService extends Schmervice.Service {
28 29
         if (Object.keys(this.tagLookup).length) return
29 30
         const { Tag } = this.server.models()
30 31
         const allTagDescriptions = await Tag.query()
31
-        allTagDescriptions.forEach(desc => {
32
-            if (!desc.is_active) return
33
-            this.tagLookup[desc.tag_id] = desc
34
-        })
32
+        if (allTagDescriptions.length) {
33
+            allTagDescriptions.forEach(desc => {
34
+                if (!desc.is_active) return
35
+                this.tagLookup[desc.tag_id] = desc
36
+            })
37
+        } else return
35 38
     }
36 39
     /**
37 40
      * Internal method to get list of profile_ids for this user
@@ -40,15 +43,12 @@ module.exports = class ProfileService extends Schmervice.Service {
40 43
      */
41 44
     async _getProfileIdsForUserId(userId) {
42 45
         const { Profile } = this.server.models()
43
-
44 46
         /** Grab every Profile associated with this id */
45 47
         const allProfiles = await Profile.query().where('user_id', userId)
46
-
47 48
         /** Copy a list of the just the Profiles */
48
-        const profileIdsToGrab = allProfiles.map(profile => profile.profile_id)
49
-
50
-        /** Uncomment to dedupe the list just in case */
51
-        return [...new Set(profileIdsToGrab)]
49
+        return typeof allProfiles === 'object' && allProfiles !== null
50
+            ? [...new Set(allProfiles.map(profile => profile.profile_id))]
51
+            : [allProfiles]
52 52
     }
53 53
 
54 54
     /**
@@ -252,7 +252,6 @@ module.exports = class ProfileService extends Schmervice.Service {
252 252
         certifications,
253 253
     ) {
254 254
         const { Profile } = this.server.models()
255
-
256 255
         await this._setScoreLookup()
257 256
 
258 257
         // Our User Profile to score for
@@ -276,10 +275,12 @@ module.exports = class ProfileService extends Schmervice.Service {
276 275
             distanceUnit,
277 276
             userZip,
278 277
         )
278
+
279 279
         matchPool = filter.byDistance(matchPool, maxDistance)
280 280
         matchPool = filter.byDuration(matchPool, duration)
281 281
         matchPool = filter.byPresence(matchPool, presence)
282
-        matchPool = filter.byCertifications(matchPool, certifications)
282
+        // TODO: Incorporate filtering by certifications (see filter.js)
283
+        // matchPool = filter.byCertifications(matchPool, certifications)
283 284
 
284 285
         const scoredProfilesWithDistance = scoring.scoreAll(
285 286
             matchPool,
@@ -293,12 +294,11 @@ module.exports = class ProfileService extends Schmervice.Service {
293 294
     }
294 295
 
295 296
     async calcProfileDistances(matchPool, distanceUnit, userZip) {
296
-        await Promise.all(
297
+        const returnVal = await Promise.all(
297 298
             matchPool.map(async profile => {
298 299
                 const targetZip = zipcoder.getZipCodeFromProfile(profile)
299 300
                 if (!userZip || !targetZip)
300 301
                     return { ...profile, distance: [9999, distanceUnit] }
301
-
302 302
                 const distance = await this._compareDistance(
303 303
                     userZip,
304 304
                     targetZip,
@@ -310,6 +310,7 @@ module.exports = class ProfileService extends Schmervice.Service {
310 310
                 }
311 311
             }),
312 312
         )
313
+        return returnVal
313 314
     }
314 315
 
315 316
     /**
@@ -324,11 +325,14 @@ module.exports = class ProfileService extends Schmervice.Service {
324 325
             parseInt(zipCode),
325 326
         )
326 327
         if (!zipInfo) {
327
-            console.error('zip:', zipCode)
328
-        }
329
-        return {
330
-            latitude: parseFloat(zipInfo.latitude),
331
-            longitude: parseFloat(zipInfo.longitude),
328
+            throw new Error(
329
+                `ERROR :=> no zipInfo found for zipCode: ${zipCode}`,
330
+            )
331
+        } else {
332
+            return {
333
+                latitude: parseFloat(zipInfo.latitude),
334
+                longitude: parseFloat(zipInfo.longitude),
335
+            }
332 336
         }
333 337
     }
334 338
     /**
@@ -339,7 +343,13 @@ module.exports = class ProfileService extends Schmervice.Service {
339 343
      * @param {number} distance in miles
340 344
      */
341 345
     async _compareDistance(start_zip, end_zip, distanceUnit) {
342
-        if (!start_zip || !end_zip || isNaN(start_zip) || isNaN(end_zip)) return
346
+        if (
347
+            !start_zip ||
348
+            !end_zip ||
349
+            Number.isNaN(start_zip) ||
350
+            Number.isNaN(end_zip)
351
+        )
352
+            return
343 353
         const start = await this._latLonForZip(start_zip)
344 354
         const end = await this._latLonForZip(end_zip)
345 355
         return haversine(start, end, { unit: distanceUnit })
@@ -356,8 +366,8 @@ module.exports = class ProfileService extends Schmervice.Service {
356 366
         await this._setTagLookup()
357 367
         let associations = groupingId
358 368
             ? await TagAssociation.query()
359
-                .where('grouping_id', groupingId)
360
-                .andWhere('profile_id', profileId)
369
+                  .where('grouping_id', groupingId)
370
+                  .andWhere('profile_id', profileId)
361 371
             : await TagAssociation.query().andWhere('profile_id', profileId)
362 372
         return associations
363 373
             .map(assoc => ({

+ 7
- 3
backend/lib/services/profile/profiler.js 查看文件

@@ -105,9 +105,13 @@ const makeOrderedCompleteProfiles = (
105 105
  * @returns {Array}
106 106
  */
107 107
 const makeCompleteFromProfileEntries = (profilesEntries, type, tagLookup) => {
108
-    return profilesEntries.map(entry =>
109
-        _makeCompleteProfile(entry, type, tagLookup),
110
-    )
108
+    try {
109
+        return profilesEntries.map(entry =>
110
+            _makeCompleteProfile(entry, type, tagLookup),
111
+        )
112
+    } catch (err) {
113
+        throw new Error(err)
114
+    }
111 115
 }
112 116
 
113 117
 module.exports = {

+ 73
- 57
backend/lib/services/user.js 查看文件

@@ -7,23 +7,15 @@ const Schmervice = require('@hapipal/schmervice')
7 7
 const SecurePassword = require('secure-password')
8 8
 
9 9
 // Configuration for Brevo
10
-const SibApiV3Sdk = require('sib-api-v3-sdk')
11
-const { access, accessSync } = require('fs')
12
-const defaultClient = SibApiV3Sdk.ApiClient.instance
10
+const Brevo = require('@getbrevo/brevo')
11
+const defaultClient = Brevo.ApiClient.instance
13 12
 const apiKey = defaultClient.authentications['api-key']
14 13
 apiKey.apiKey = process.env.BREVO_KEY
14
+const apiInstance = new Brevo.TransactionalEmailsApi()
15
+const sendSmtpEmail = new Brevo.SendSmtpEmail()
15 16
 
16
-const apiInstance = new SibApiV3Sdk.TransactionalEmailsApi()
17
-
18
-const hashToken = async token => {
19
-    const salt = process.env.APP_SESSION_SALT
20
-    try {
21
-        return crypto.createHmac('sha256', salt).update(token).digest('hex')
22
-    } catch (err) {
23
-        throw new Error(err.message)
24
-    }
25
-}
26
-
17
+// TODO: Consider implementing, nice use of SecurePassword,
18
+// but currently not used anywhere...
27 19
 const hasher = async (pwd, steak) => {
28 20
     const hash = await pwd.hash(steak)
29 21
     const result = await pwd.verify(steak, hash)
@@ -66,24 +58,7 @@ module.exports = class UserService extends Schmervice.Service {
66 58
         const pwd = new SecurePassword()
67 59
         // TODO: Invalidate this application state somehow after a
68 60
         // certain time period has passed
69
-        this.activeSessions = {
70
-            // abc123456: '123456689',
71
-            // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: {
72
-            // email: rawEmailString,
73
-            // name: 'Joe Doe',
74
-            // seeking: 'candidate'
75
-            // sessionToken: rawSessionToken, // use for expires instead of expires?
76
-            // expires: expirationTime in seconds
77
-            // }
78
-        }
79
-        // Check the hashedCookie which is our hashedSessionToken string
80
-        // validate whether or not the rawAccessToken is still valid, if valid good to go.
81
-        // if NOT valid, then we need to reassign accessToken to a newAccessToken
82
-        // this.activeSessions = {
83
-        // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: {
84
-        // accessToken: 'as;dflkja;;dlfkja;sldkf... rawAccessToken'
85
-        // }
86
-        // }
61
+        this.activeSessions = {}
87 62
 
88 63
         this.pwd = {
89 64
             hash: Util.promisify(pwd.hash.bind(pwd)),
@@ -136,6 +111,15 @@ module.exports = class UserService extends Schmervice.Service {
136 111
         return user
137 112
     }
138 113
 
114
+    hashToken(token) {
115
+        const salt = process.env.APP_SESSION_SALT
116
+        try {
117
+            return crypto.createHmac('sha256', salt).update(token).digest('hex')
118
+        } catch (err) {
119
+            throw new Error(err.message)
120
+        }
121
+    }
122
+
139 123
     /**
140 124
      * Signup function
141 125
      * @param {*} param0
@@ -208,11 +192,11 @@ module.exports = class UserService extends Schmervice.Service {
208 192
             .throwIfNotFound()
209 193
             .first()
210 194
             .where({ user_email: email })
211
-
212 195
         const bufferPepper = Buffer.from(process.env.PEPPER + password)
213 196
 
214 197
         /** Uncomment to run password check using SecurePassword */
215 198
         const passwordCheck = await this.pwd.verify(bufferPepper, user.token)
199
+
216 200
         if (passwordCheck === SecurePassword.VALID_NEEDS_REHASH) {
217 201
             await this.changePassword(user.user_email, password, txn)
218 202
         } else if (passwordCheck !== SecurePassword.VALID) {
@@ -234,6 +218,22 @@ module.exports = class UserService extends Schmervice.Service {
234 218
         return JWT.sign(obj, key, { expiresIn: expiration })
235 219
     }
236 220
 
221
+    async makeUserCredentials(email) {
222
+        const user = await this.findByUserEmail(email)
223
+        const userCredentials = {
224
+            email: user.user_email,
225
+            name: user.user_name,
226
+            seeking: user.is_poster === 1 ? 'poster' : 'seeker',
227
+        }
228
+        const token = this.createToken({
229
+            payload: userCredentials,
230
+        })
231
+        return {
232
+            userCredentials,
233
+            token,
234
+        }
235
+    }
236
+
237 237
     /**
238 238
      * Validates whether a token has expired or not
239 239
      * @param {User} user
@@ -253,18 +253,31 @@ module.exports = class UserService extends Schmervice.Service {
253 253
      * @param {HashedSessionToken} hashedSessionToken
254 254
      * @returns {PayloadFromActiveSessions}
255 255
      */
256
-    validateSession(hashedAccessToken) {
257
-        const userSession = this.activeSessions[hashedAccessToken]
256
+    validateSession(hashedSessionToken) {
257
+        const userSession = this.activeSessions[hashedSessionToken]
258 258
         if (!userSession) {
259 259
             throw new Error(
260 260
                 'hashedSessionToken not in activeSessions registry!',
261 261
             )
262 262
         }
263
-        const accessToken = userSession.accessToken
264
-        const accessTokenIsValid = this.validateToken(accessToken)
263
+        if (!userSession.emailWasRespondedTo) {
264
+            throw new Error('email was never responded to!')
265
+        }
266
+        const sessionToken = userSession.sessionToken
267
+        const sessionTokenIsValid = this.validateToken(sessionToken)
265 268
         return {
266
-            ...accessTokenIsValid.payload,
267
-            accessToken: this.activeSessions[hashedAccessToken].accessToken,
269
+            ...sessionTokenIsValid.payload,
270
+            sessionToken: this.activeSessions[hashedSessionToken].sessionToken,
271
+        }
272
+    }
273
+    removeSession(hashedSessionToken) {
274
+        const userSession = this.activeSessions[hashedSessionToken]
275
+        if (!userSession) {
276
+            throw new Error(
277
+                'hashedSessionToken not in activeSessions registry!',
278
+            )
279
+        } else {
280
+            delete this.activeSessions[hashedSessionToken]
268 281
         }
269 282
     }
270 283
     /**
@@ -297,7 +310,6 @@ module.exports = class UserService extends Schmervice.Service {
297 310
         const passwordRow = await Auth.query(txn)
298 311
             .where('user_email', email)
299 312
             .first()
300
-
301 313
         return passwordRow ? passwordRow.token : null
302 314
     }
303 315
 
@@ -306,35 +318,39 @@ module.exports = class UserService extends Schmervice.Service {
306 318
      * @ returns {Object}
307 319
      */
308 320
     async emailSent(userCredentials) {
309
-        const hashedAccessToken = await hashToken(userCredentials.accessToken)
310
-        if (Object.keys(this.activeSessions).includes(hashedAccessToken)) {
321
+        const hashedSessionToken = this.hashToken(userCredentials.sessionToken)
322
+        if (Object.keys(this.activeSessions).includes(hashedSessionToken)) {
311 323
             return new Error('session already in cache!!')
312 324
         }
313 325
         // Set expiration time for ten minutes from now
314 326
         const duration = 600000
315 327
 
316
-        this.activeSessions[hashedAccessToken] = {
328
+        this.activeSessions[hashedSessionToken] = {
317 329
             email: userCredentials.email,
318 330
             name: userCredentials.name,
319 331
             seeking: userCredentials.seeking,
320
-            accessToken: userCredentials.accessToken,
332
+            sessionToken: userCredentials.sessionToken,
321 333
             expiration: Date.now() + duration,
322
-            sessionToken: null,
334
+            emailWasRespondedTo: false,
335
+            accessToken: null,
323 336
         }
324
-
325
-        const sendSmtpEmail = {
326
-            to: [
327
-                {
328
-                    email: userCredentials.email,
329
-                },
330
-            ],
331
-            templateId: 2,
332
-            params: {
333
-                // TODO: Change this in production...
334
-                link: `localhost:3000/verify/${hashedAccessToken}`,
337
+        // NOTE: Although this looks messy, Brevo requries these
338
+        // parameters be defined individually like this, attempts
339
+        // to configure this in a singel object cause errors on their API
340
+        sendSmtpEmail.sender = {
341
+            name: 'My Test Company',
342
+            email: 'mytestemail@email.com',
343
+        }
344
+        sendSmtpEmail.subject = 'My Test Company'
345
+        sendSmtpEmail.to = [
346
+            {
347
+                email: userCredentials.email,
335 348
             },
349
+        ]
350
+        sendSmtpEmail.templateId = Number(process.env.BREVO_TEMPLATE_ID)
351
+        sendSmtpEmail.params = {
352
+            link: `${process.env.BREVO_LINK}/verify/${hashedSessionToken}`,
336 353
         }
337
-
338 354
         return await apiInstance.sendTransacEmail(sendSmtpEmail).then(
339 355
             data => {
340 356
                 return { wasSuccessfull: true, data: data }

+ 175
- 58
backend/package-lock.json 查看文件

@@ -9,6 +9,7 @@
9 9
             "version": "1.0.0",
10 10
             "license": "UNLICENSED",
11 11
             "dependencies": {
12
+                "@getbrevo/brevo": "^1.0.1",
12 13
                 "@hapi/glue": "^8.0.0",
13 14
                 "@hapi/hapi": "^20.1.3",
14 15
                 "@hapi/inert": "^6.0.3",
@@ -28,8 +29,7 @@
28 29
                 "knex": "^0.21.19",
29 30
                 "mysql": "^2.18.1",
30 31
                 "objection": "^2.2.18",
31
-                "secure-password": "^4.0.0",
32
-                "sib-api-v3-sdk": "^8.5.0"
32
+                "secure-password": "^4.0.0"
33 33
             },
34 34
             "devDependencies": {
35 35
                 "ava": "^3.15.0",
@@ -577,6 +577,15 @@
577 577
                 "url": "https://github.com/sponsors/sindresorhus"
578 578
             }
579 579
         },
580
+        "node_modules/@getbrevo/brevo": {
581
+            "version": "1.0.1",
582
+            "resolved": "https://registry.npmjs.org/@getbrevo/brevo/-/brevo-1.0.1.tgz",
583
+            "integrity": "sha512-NwUOlkft6NwLSKTph9FWQujMM5ysSGWOa9Wdf0Bc/RezejOW5VG5KXvTmXrF1Q+MHmjzTh6GDWjq5EukQsDdnA==",
584
+            "dependencies": {
585
+                "querystring": "^0.2.1",
586
+                "superagent": "^8.0.9"
587
+            }
588
+        },
580 589
         "node_modules/@hapi/accept": {
581 590
             "version": "5.0.2",
582 591
             "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-5.0.2.tgz",
@@ -1409,6 +1418,11 @@
1409 1418
                 "node": ">=8"
1410 1419
             }
1411 1420
         },
1421
+        "node_modules/asap": {
1422
+            "version": "2.0.6",
1423
+            "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
1424
+            "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
1425
+        },
1412 1426
         "node_modules/assign-symbols": {
1413 1427
             "version": "1.0.0",
1414 1428
             "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -1876,12 +1890,13 @@
1876 1890
             }
1877 1891
         },
1878 1892
         "node_modules/call-bind": {
1879
-            "version": "1.0.2",
1880
-            "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
1881
-            "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
1893
+            "version": "1.0.5",
1894
+            "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
1895
+            "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
1882 1896
             "dependencies": {
1883
-                "function-bind": "^1.1.1",
1884
-                "get-intrinsic": "^1.0.2"
1897
+                "function-bind": "^1.1.2",
1898
+                "get-intrinsic": "^1.2.1",
1899
+                "set-function-length": "^1.1.1"
1885 1900
             },
1886 1901
             "funding": {
1887 1902
                 "url": "https://github.com/sponsors/ljharb"
@@ -2481,6 +2496,7 @@
2481 2496
             "version": "3.2.7",
2482 2497
             "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
2483 2498
             "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
2499
+            "dev": true,
2484 2500
             "dependencies": {
2485 2501
                 "ms": "^2.1.1"
2486 2502
             }
@@ -2565,6 +2581,19 @@
2565 2581
             "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
2566 2582
             "dev": true
2567 2583
         },
2584
+        "node_modules/define-data-property": {
2585
+            "version": "1.1.1",
2586
+            "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
2587
+            "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
2588
+            "dependencies": {
2589
+                "get-intrinsic": "^1.2.1",
2590
+                "gopd": "^1.0.1",
2591
+                "has-property-descriptors": "^1.0.0"
2592
+            },
2593
+            "engines": {
2594
+                "node": ">= 0.4"
2595
+            }
2596
+        },
2568 2597
         "node_modules/define-property": {
2569 2598
             "version": "2.0.2",
2570 2599
             "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
@@ -2615,6 +2644,15 @@
2615 2644
                 "node": ">=0.10.0"
2616 2645
             }
2617 2646
         },
2647
+        "node_modules/dezalgo": {
2648
+            "version": "1.0.4",
2649
+            "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
2650
+            "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
2651
+            "dependencies": {
2652
+                "asap": "^2.0.0",
2653
+                "wrappy": "1"
2654
+            }
2655
+        },
2618 2656
         "node_modules/diff": {
2619 2657
             "version": "5.0.0",
2620 2658
             "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
@@ -3370,6 +3408,11 @@
3370 3408
             "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
3371 3409
             "dev": true
3372 3410
         },
3411
+        "node_modules/fast-safe-stringify": {
3412
+            "version": "2.1.1",
3413
+            "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
3414
+            "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
3415
+        },
3373 3416
         "node_modules/fastq": {
3374 3417
             "version": "1.11.0",
3375 3418
             "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz",
@@ -3536,16 +3579,16 @@
3536 3579
             }
3537 3580
         },
3538 3581
         "node_modules/form-data": {
3539
-            "version": "2.5.1",
3540
-            "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
3541
-            "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
3582
+            "version": "4.0.0",
3583
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
3584
+            "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
3542 3585
             "dependencies": {
3543 3586
                 "asynckit": "^0.4.0",
3544
-                "combined-stream": "^1.0.6",
3587
+                "combined-stream": "^1.0.8",
3545 3588
                 "mime-types": "^2.1.12"
3546 3589
             },
3547 3590
             "engines": {
3548
-                "node": ">= 0.12"
3591
+                "node": ">= 6"
3549 3592
             }
3550 3593
         },
3551 3594
         "node_modules/format-util": {
@@ -3554,10 +3597,15 @@
3554 3597
             "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg=="
3555 3598
         },
3556 3599
         "node_modules/formidable": {
3557
-            "version": "1.2.6",
3558
-            "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
3559
-            "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
3560
-            "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
3600
+            "version": "2.1.2",
3601
+            "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
3602
+            "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
3603
+            "dependencies": {
3604
+                "dezalgo": "^1.0.4",
3605
+                "hexoid": "^1.0.0",
3606
+                "once": "^1.4.0",
3607
+                "qs": "^6.11.0"
3608
+            },
3561 3609
             "funding": {
3562 3610
                 "url": "https://ko-fi.com/tunnckoCore/commissions"
3563 3611
             }
@@ -3614,9 +3662,12 @@
3614 3662
             }
3615 3663
         },
3616 3664
         "node_modules/function-bind": {
3617
-            "version": "1.1.1",
3618
-            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
3619
-            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
3665
+            "version": "1.1.2",
3666
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
3667
+            "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
3668
+            "funding": {
3669
+                "url": "https://github.com/sponsors/ljharb"
3670
+            }
3620 3671
         },
3621 3672
         "node_modules/functional-red-black-tree": {
3622 3673
             "version": "1.0.1",
@@ -3803,6 +3854,17 @@
3803 3854
                 "url": "https://github.com/sponsors/sindresorhus"
3804 3855
             }
3805 3856
         },
3857
+        "node_modules/gopd": {
3858
+            "version": "1.0.1",
3859
+            "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
3860
+            "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
3861
+            "dependencies": {
3862
+                "get-intrinsic": "^1.1.3"
3863
+            },
3864
+            "funding": {
3865
+                "url": "https://github.com/sponsors/ljharb"
3866
+            }
3867
+        },
3806 3868
         "node_modules/got": {
3807 3869
             "version": "9.6.0",
3808 3870
             "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
@@ -3918,6 +3980,17 @@
3918 3980
                 "node": ">=4"
3919 3981
             }
3920 3982
         },
3983
+        "node_modules/has-property-descriptors": {
3984
+            "version": "1.0.0",
3985
+            "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
3986
+            "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
3987
+            "dependencies": {
3988
+                "get-intrinsic": "^1.1.1"
3989
+            },
3990
+            "funding": {
3991
+                "url": "https://github.com/sponsors/ljharb"
3992
+            }
3993
+        },
3921 3994
         "node_modules/has-proto": {
3922 3995
             "version": "1.0.1",
3923 3996
             "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
@@ -4028,6 +4101,14 @@
4028 4101
             "resolved": "https://registry.npmjs.org/haversine/-/haversine-1.1.1.tgz",
4029 4102
             "integrity": "sha512-KW4MS8+krLIeiw8bF5z532CptG0ZyGGFj0UbKMxx25lKnnJ1hMUbuzQl+PXQjNiDLnl1bOyz23U6hSK10r4guw=="
4030 4103
         },
4104
+        "node_modules/hexoid": {
4105
+            "version": "1.0.0",
4106
+            "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
4107
+            "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
4108
+            "engines": {
4109
+                "node": ">=8"
4110
+            }
4111
+        },
4031 4112
         "node_modules/homedir-polyfill": {
4032 4113
             "version": "1.0.3",
4033 4114
             "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -5337,14 +5418,14 @@
5337 5418
             }
5338 5419
         },
5339 5420
         "node_modules/mime": {
5340
-            "version": "1.6.0",
5341
-            "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
5342
-            "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
5421
+            "version": "2.6.0",
5422
+            "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
5423
+            "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
5343 5424
             "bin": {
5344 5425
                 "mime": "cli.js"
5345 5426
             },
5346 5427
             "engines": {
5347
-                "node": ">=4"
5428
+                "node": ">=4.0.0"
5348 5429
             }
5349 5430
         },
5350 5431
         "node_modules/mime-db": {
@@ -5830,9 +5911,9 @@
5830 5911
             }
5831 5912
         },
5832 5913
         "node_modules/object-inspect": {
5833
-            "version": "1.12.3",
5834
-            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
5835
-            "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
5914
+            "version": "1.13.1",
5915
+            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
5916
+            "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
5836 5917
             "funding": {
5837 5918
                 "url": "https://github.com/sponsors/ljharb"
5838 5919
             }
@@ -5904,7 +5985,6 @@
5904 5985
             "version": "1.4.0",
5905 5986
             "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
5906 5987
             "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
5907
-            "dev": true,
5908 5988
             "dependencies": {
5909 5989
                 "wrappy": "1"
5910 5990
             }
@@ -6499,9 +6579,9 @@
6499 6579
             }
6500 6580
         },
6501 6581
         "node_modules/querystring": {
6502
-            "version": "0.2.0",
6503
-            "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
6504
-            "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
6582
+            "version": "0.2.1",
6583
+            "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz",
6584
+            "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==",
6505 6585
             "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
6506 6586
             "engines": {
6507 6587
                 "node": ">=0.4.x"
@@ -6935,6 +7015,20 @@
6935 7015
             "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
6936 7016
             "dev": true
6937 7017
         },
7018
+        "node_modules/set-function-length": {
7019
+            "version": "1.1.1",
7020
+            "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
7021
+            "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
7022
+            "dependencies": {
7023
+                "define-data-property": "^1.1.1",
7024
+                "get-intrinsic": "^1.2.1",
7025
+                "gopd": "^1.0.1",
7026
+                "has-property-descriptors": "^1.0.0"
7027
+            },
7028
+            "engines": {
7029
+                "node": ">= 0.4"
7030
+            }
7031
+        },
6938 7032
         "node_modules/set-value": {
6939 7033
             "version": "2.0.1",
6940 7034
             "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -6989,16 +7083,6 @@
6989 7083
                 "node": ">=8"
6990 7084
             }
6991 7085
         },
6992
-        "node_modules/sib-api-v3-sdk": {
6993
-            "version": "8.5.0",
6994
-            "resolved": "https://registry.npmjs.org/sib-api-v3-sdk/-/sib-api-v3-sdk-8.5.0.tgz",
6995
-            "integrity": "sha512-6Ratp5kLN/rEEvk4XVIQ4L8IrCIrcfE9m1HjvHz/WepC+CVXPsjOlgRcK/jQjpN5kC+dmhDAqrTo1OtnF6i1wA==",
6996
-            "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
6997
-            "dependencies": {
6998
-                "querystring": "0.2.0",
6999
-                "superagent": "3.7.0"
7000
-            }
7001
-        },
7002 7086
         "node_modules/side-channel": {
7003 7087
             "version": "1.0.4",
7004 7088
             "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -7578,24 +7662,58 @@
7578 7662
             }
7579 7663
         },
7580 7664
         "node_modules/superagent": {
7581
-            "version": "3.7.0",
7582
-            "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.7.0.tgz",
7583
-            "integrity": "sha512-/8trxO6NbLx4YXb7IeeFTSmsQ35pQBiTBsLNvobZx7qBzBeHYvKCyIIhW2gNcWbLzYxPAjdgFbiepd8ypwC0Gw==",
7584
-            "deprecated": "Please upgrade to v7.0.2+ of superagent.  We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing.  See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>.",
7585
-            "dependencies": {
7586
-                "component-emitter": "^1.2.0",
7587
-                "cookiejar": "^2.1.0",
7588
-                "debug": "^3.1.0",
7589
-                "extend": "^3.0.0",
7590
-                "form-data": "^2.3.1",
7591
-                "formidable": "^1.1.1",
7592
-                "methods": "^1.1.1",
7593
-                "mime": "^1.4.1",
7594
-                "qs": "^6.5.1",
7595
-                "readable-stream": "^2.0.5"
7665
+            "version": "8.1.2",
7666
+            "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
7667
+            "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==",
7668
+            "dependencies": {
7669
+                "component-emitter": "^1.3.0",
7670
+                "cookiejar": "^2.1.4",
7671
+                "debug": "^4.3.4",
7672
+                "fast-safe-stringify": "^2.1.1",
7673
+                "form-data": "^4.0.0",
7674
+                "formidable": "^2.1.2",
7675
+                "methods": "^1.1.2",
7676
+                "mime": "2.6.0",
7677
+                "qs": "^6.11.0",
7678
+                "semver": "^7.3.8"
7596 7679
             },
7597 7680
             "engines": {
7598
-                "node": ">= 4.0"
7681
+                "node": ">=6.4.0 <13 || >=14"
7682
+            }
7683
+        },
7684
+        "node_modules/superagent/node_modules/debug": {
7685
+            "version": "4.3.4",
7686
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
7687
+            "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
7688
+            "dependencies": {
7689
+                "ms": "2.1.2"
7690
+            },
7691
+            "engines": {
7692
+                "node": ">=6.0"
7693
+            },
7694
+            "peerDependenciesMeta": {
7695
+                "supports-color": {
7696
+                    "optional": true
7697
+                }
7698
+            }
7699
+        },
7700
+        "node_modules/superagent/node_modules/ms": {
7701
+            "version": "2.1.2",
7702
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
7703
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
7704
+        },
7705
+        "node_modules/superagent/node_modules/semver": {
7706
+            "version": "7.5.4",
7707
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
7708
+            "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
7709
+            "dependencies": {
7710
+                "lru-cache": "^6.0.0"
7711
+            },
7712
+            "bin": {
7713
+                "semver": "bin/semver.js"
7714
+            },
7715
+            "engines": {
7716
+                "node": ">=10"
7599 7717
             }
7600 7718
         },
7601 7719
         "node_modules/supertap": {
@@ -8200,8 +8318,7 @@
8200 8318
         "node_modules/wrappy": {
8201 8319
             "version": "1.0.2",
8202 8320
             "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
8203
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
8204
-            "dev": true
8321
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
8205 8322
         },
8206 8323
         "node_modules/write-file-atomic": {
8207 8324
             "version": "3.0.3",

+ 3
- 3
backend/package.json 查看文件

@@ -12,11 +12,12 @@
12 12
         "reseed": "knex migrate:rollback --all && knex migrate:latest && knex seed:run",
13 13
         "generate": "node ./db/data-generator/index.js",
14 14
         "seed": "knex seed:run",
15
-        "test": "nyc ava --timeout=100000"
15
+        "test": "nyc ava --timeout=100000 --verbose"
16 16
     },
17 17
     "author": "TOJ",
18 18
     "license": "UNLICENSED",
19 19
     "dependencies": {
20
+        "@getbrevo/brevo": "^1.0.1",
20 21
         "@hapi/glue": "^8.0.0",
21 22
         "@hapi/hapi": "^20.1.3",
22 23
         "@hapi/inert": "^6.0.3",
@@ -36,8 +37,7 @@
36 37
         "knex": "^0.21.19",
37 38
         "mysql": "^2.18.1",
38 39
         "objection": "^2.2.18",
39
-        "secure-password": "^4.0.0",
40
-        "sib-api-v3-sdk": "^8.5.0"
40
+        "secure-password": "^4.0.0"
41 41
     },
42 42
     "devDependencies": {
43 43
         "ava": "^3.15.0",

+ 74
- 0
backend/tests/user-auth-pass.spec.js 查看文件

@@ -0,0 +1,74 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const UserService = require('../lib/services/user.js')
7
+
8
+const plugin = require('../lib/plugins/user.js')
9
+const Auth = require('../lib/models/authentication.js')
10
+
11
+const params = {
12
+    user_email: 'test@testemail.com',
13
+}
14
+
15
+const mockReturn = {
16
+    password: 'a;slkdfja;ldfjk;alkdsfja;lsdkfj',
17
+}
18
+
19
+const pathToTest = {
20
+    method: 'GET',
21
+    url: `/${params.user_email}/password`,
22
+}
23
+
24
+test(`path /${params.user_email}/password should return OK on GET`, async t => {
25
+    /**
26
+     * Create a new server and register services,
27
+     * models and routes for testing
28
+     * -
29
+     * NOTE: We use a mocked registerModel() and register
30
+     * models manually. Normally this is handled by
31
+     * Schwifty at runtime.
32
+     */
33
+    const server = Hapi.server()
34
+    /**
35
+     * Overload so we don't register any models
36
+     * using the plugin call (see plugins/profile.js)
37
+     * and Manually load the model we need for the test
38
+     */
39
+
40
+    server.models = () => ({ Auth })
41
+    // TODO: Apply server.registrations to other test specs
42
+    server.registrations = {
43
+        'main-app-plugin': {
44
+            options: {},
45
+        },
46
+    }
47
+
48
+    /**
49
+     * Register Routes and Services as usual
50
+     */
51
+    await plugin.register(server)
52
+    server.services()['userService'] = new UserService(server)
53
+
54
+    /**
55
+     * Replace Objection model methods with our own mock functions
56
+     * !: Janky - might be better to temp knex sqlite instance
57
+     */
58
+    stub(server.models()['Auth'], 'query').returns({
59
+        where: () => ({
60
+            first: () => ({
61
+                token: 'a;slkdfja;ldfjk;alkdsfja;lsdkfj',
62
+            }),
63
+        }),
64
+    })
65
+
66
+    /**
67
+     * Test the server with registered models and services
68
+     */
69
+    const { payload } = await server.inject(pathToTest)
70
+    const res = JSON.parse(payload)
71
+    t.deepEqual(res.ok, true)
72
+    t.deepEqual(res.data, mockReturn)
73
+    server.stop()
74
+})

+ 112
- 0
backend/tests/user-create-profile.spec.js 查看文件

@@ -0,0 +1,112 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const UserService = require('../lib/services/user.js')
7
+const ProfileService = require('../lib/services/profile/index.js')
8
+
9
+const plugin = require('../lib/plugins/user.js')
10
+const User = require('../lib/models/user.js')
11
+const Profile = require('../lib/models/profile.js')
12
+const Tag = require('../lib/models/tag.js')
13
+const Response = require('../lib/models/response.js')
14
+
15
+const params = {
16
+    user_id: 102,
17
+}
18
+const payload = [
19
+    { response_key_id: 8, val: 'bobby@bob.com' },
20
+    { response_key_id: 9, val: 'password' },
21
+    { response_key_id: 7, val: 'fk' },
22
+    { response_key_id: 11, val: 'position' },
23
+]
24
+
25
+const mockProfile = {
26
+    user_id: 102,
27
+    id: 147,
28
+}
29
+
30
+const mockReturn = {
31
+    user_id: 102,
32
+    profile_id: 147,
33
+}
34
+
35
+const pathToTest = {
36
+    method: 'POST',
37
+    url: `/${params.user_id}/profile`,
38
+    payload: JSON.stringify(payload),
39
+}
40
+
41
+test('path /<user_id>/profile should return new profile info', async t => {
42
+    /**
43
+     * Create a new server and register services,
44
+     * models and routes for testing
45
+     * -
46
+     * NOTE: We use register models manually.
47
+     * Normally this is handled by
48
+     * Schwifty at runtime.
49
+     */
50
+    const server = Hapi.server()
51
+    server.registrations = {
52
+        'main-app-plugin': {
53
+            options: {},
54
+        },
55
+    }
56
+    /**
57
+     * Register Routes and Services as usual
58
+     */
59
+    await plugin.register(server)
60
+
61
+    server.models = () => ({
62
+        User,
63
+        Profile,
64
+        Tag,
65
+        Response,
66
+    })
67
+
68
+    server.services()['userService'] = new UserService(server)
69
+    server.services()['profileService'] = new ProfileService(server)
70
+
71
+    /**
72
+     * Replace Objection model methods with our own mock functions
73
+     * !: Janky - might be better to temp knex sqlite instance
74
+     */
75
+    stub(server.models()['User'], 'query').returns({
76
+        throwIfNotFound: () => ({
77
+            first: () => ({
78
+                where: () => mockProfile.user_id,
79
+            }),
80
+        }),
81
+    })
82
+
83
+    stub(server.models()['Tag'], 'query').returns({})
84
+    stub(server.models()['Profile'], 'query').returns({
85
+        where: () => mockProfile.user_id,
86
+        whereIn: () => ({
87
+            withGraphFetched: () => ({
88
+                withGraphFetched: () => ({
89
+                    withGraphFetched: () => [],
90
+                }),
91
+            }),
92
+        }),
93
+        insert: () => mockProfile,
94
+    })
95
+
96
+    stub(server.models()['Response'], 'query').returns({
97
+        insert: () => ({
98
+            profile_id: 147,
99
+            response_key_id: 11,
100
+            val: 'position',
101
+            id: 3,
102
+        }),
103
+    })
104
+    /**
105
+     * Test the server with registered models and services
106
+     */
107
+    const { payload } = await server.inject(pathToTest)
108
+    const res = JSON.parse(payload)
109
+    t.deepEqual(res.ok, true)
110
+    t.deepEqual(res.data, mockReturn)
111
+    server.stop()
112
+})

+ 64
- 0
backend/tests/user-create-token.spec.js 查看文件

@@ -0,0 +1,64 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const UserService = require('../lib/services/user.js')
7
+
8
+const plugin = require('../lib/plugins/user.js')
9
+
10
+const JWT = require('jsonwebtoken')
11
+
12
+const payload = {
13
+    email: 'test@testemail.com',
14
+    name: 'fk',
15
+    seeking: 'seeker',
16
+}
17
+
18
+const data = payload
19
+
20
+const pathToTest = {
21
+    method: 'POST',
22
+    url: '/token',
23
+    payload: JSON.stringify(payload),
24
+}
25
+
26
+test('path /token should return OK', async t => {
27
+    /**
28
+     * Create a new server and register services,
29
+     * models and routes for testing
30
+     */
31
+    const server = Hapi.server()
32
+    server.registrations = {
33
+        'main-app-plugin': {
34
+            options: {},
35
+        },
36
+    }
37
+
38
+    /**
39
+     * Register Services
40
+     */
41
+    await plugin.register(server)
42
+    server.services()['userService'] = new UserService(server)
43
+
44
+    const createToken = (data, expiration = 600) => {
45
+        const key = process.env.APP_SECRET
46
+        const obj = {}
47
+        Object.assign(obj, { ...data })
48
+        return JWT.sign(obj, key, { expiresIn: expiration })
49
+    }
50
+
51
+    stub(server.services()['userService'], 'createToken').returns(
52
+        createToken(data),
53
+    )
54
+
55
+    /**
56
+     * Test the server with registered models and services
57
+     */
58
+    const { payload } = await server.inject(pathToTest)
59
+    const res = JSON.parse(payload)
60
+    t.deepEqual(res.ok, true)
61
+    t.assert(res.data)
62
+    t.is(res.data.error, undefined)
63
+    server.stop()
64
+})

+ 111
- 0
backend/tests/user-credential.spec.js 查看文件

@@ -0,0 +1,111 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const Objection = require('objection')
7
+const UserService = require('../lib/services/user.js')
8
+
9
+const plugin = require('../lib/plugins/user.js')
10
+
11
+const Auth = require('../lib/models/authentication.js')
12
+const User = require('../lib/models/user.js')
13
+
14
+/**
15
+ * Route parameters
16
+ */
17
+const payload = {
18
+    user_email: 'test@testemail.com',
19
+    password: 'abcd123',
20
+}
21
+
22
+const mockReturn = {
23
+    user_id: 1234,
24
+    user_name: 'brian',
25
+    user_email: 'test@testemail.com',
26
+    is_poster: 1,
27
+    is_admin: 0,
28
+    is_verified: 1,
29
+}
30
+
31
+const pathToTest = {
32
+    method: 'POST',
33
+    url: `/login`,
34
+    payload: JSON.stringify(payload),
35
+}
36
+
37
+test('path /login should return ok', async t => {
38
+    /**
39
+     * Create a new server and register services,
40
+     * models and routes for testing
41
+     * -
42
+     * NOTE: We use a mocked registerModel() and register
43
+     * models manually. Normally this is handled by
44
+     * Schwifty at runtime.
45
+     */
46
+    const server = Hapi.server()
47
+    /**
48
+     * Overload so we don't register any models
49
+     * using the plugin call (see plugins/profile.js)
50
+     * and Manually load the model we need for the test
51
+     */
52
+    server.registerModel = () => {}
53
+
54
+    server.models = () => ({ User, Auth })
55
+    server.registrations = {
56
+        'main-app-plugin': {
57
+            options: {},
58
+        },
59
+    }
60
+    server.registrations['main-app-plugin'].options.jwtKey = {
61
+        $filter: 'NODE_ENV',
62
+        $default: {
63
+            $param: 'APP_SECRET',
64
+            $default: 'app-secret',
65
+        },
66
+        // Use .env file in production
67
+        production: {
68
+            $param: 'APP_SECRET',
69
+        },
70
+    }
71
+    stub(Objection, 'transaction').returns({})
72
+    /**
73
+     * Register Routes and Services as usual
74
+     */
75
+    await plugin.register(server)
76
+    server.services()['userService'] = new UserService(server)
77
+    server.services()['userService'].createToken = () =>
78
+        'a;slkdf;asdfa;sdfkja;lsdfj;askdfj;laskdjf;laskjdf'
79
+
80
+    /**
81
+     * Replace Objection model methods with our own mock functions
82
+     * !: Janky - might be better to temp knex sqlite instance
83
+     */
84
+
85
+    stub(server.models()['Auth'], 'query').returns({
86
+        throwIfNotFound: () => ({
87
+            first: () => ({
88
+                where: () => ({ ...mockReturn }),
89
+            }),
90
+        }),
91
+    })
92
+    stub(server.models()['User'], 'createNotFoundError').returns({})
93
+    stub(server.models()['User'], 'query').returns({
94
+        throwIfNotFound: () => ({
95
+            first: () => ({
96
+                where: () => ({ ...mockReturn }),
97
+            }),
98
+        }),
99
+    })
100
+
101
+    /**
102
+     * Test the server with registered models and services
103
+     */
104
+    const { payload } = await server.inject(pathToTest)
105
+    const res = JSON.parse(payload)
106
+
107
+    t.deepEqual(res.ok, true)
108
+    t.deepEqual(res.data.answered.email, mockReturn.user_email)
109
+    t.deepEqual(res.data.answered.name, mockReturn.user_name)
110
+    t.deepEqual(res.data.answered.seeking, 'poster')
111
+})

+ 76
- 0
backend/tests/user-remove-session.spec.js 查看文件

@@ -0,0 +1,76 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const UserService = require('../lib/services/user.js')
7
+
8
+const plugin = require('../lib/plugins/user.js')
9
+
10
+const payload = 'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk'
11
+const hashedSessionToken = 'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk'
12
+
13
+const activeSessions = {
14
+    'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk': {
15
+        email: 'test@testemail.com',
16
+        name: 'john_doe',
17
+        seeking: 'position',
18
+        sessionToken: 'efasdf;laksdfja;lkdjfa;lkdjf',
19
+        expiration: Date.now() + 600000,
20
+        emailWasRespondedTo: false,
21
+        accessToken: null,
22
+    },
23
+}
24
+
25
+const mockReturn = {
26
+    sessionTokenIsRemoved: true,
27
+}
28
+
29
+const pathToTest = {
30
+    method: 'POST',
31
+    url: '/remove-session',
32
+    payload: JSON.stringify(payload),
33
+}
34
+
35
+test('path /remove-session should return confirmation of removed session', async t => {
36
+    /**
37
+     * Create a new server and register services,
38
+     * models and routes for testing
39
+     */
40
+    const server = Hapi.server()
41
+    server.registrations = {
42
+        'main-app-plugin': {
43
+            options: {},
44
+        },
45
+    }
46
+
47
+    /**
48
+     * Register Services
49
+     */
50
+    await plugin.register(server)
51
+    server.services()['userService'] = new UserService(server)
52
+
53
+    const removeSession = hashedSessionToken => {
54
+        const userSession = activeSessions[hashedSessionToken]
55
+        if (!userSession) {
56
+            throw new Error(
57
+                'hashedSessionToken not in activeSessions registry!',
58
+            )
59
+        } else {
60
+            delete activeSessions[hashedSessionToken]
61
+        }
62
+    }
63
+
64
+    stub(server.services()['userService'], 'removeSession').returns(
65
+        removeSession(hashedSessionToken),
66
+    )
67
+
68
+    /**
69
+     * Test the server with registered models and services
70
+     */
71
+    const { payload } = await server.inject(pathToTest)
72
+    const res = JSON.parse(payload)
73
+    t.deepEqual(res.ok, true)
74
+    t.deepEqual(res.data, mockReturn)
75
+    server.stop()
76
+})

+ 147
- 0
backend/tests/user-send-email.spec.js 查看文件

@@ -0,0 +1,147 @@
1
+'use strict'
2
+
3
+/*
4
+ * NOTE: This test ACTUALLY will send an email
5
+ * (see commented out email variable below)
6
+ */
7
+
8
+// Change email here to your actual email
9
+// const email = 'myalias@myactualemail.com'
10
+
11
+const test = require('ava')
12
+const { stub } = require('sinon')
13
+const Hapi = require('@hapi/hapi')
14
+const UserService = require('../lib/services/user.js')
15
+const plugin = require('../lib/plugins/user.js')
16
+
17
+// Necessary Dependencies/Configurations for Brevo Transac Email
18
+const crypto = require('crypto')
19
+const SibApiV3Sdk = require('sib-api-v3-sdk')
20
+const defaultClient = SibApiV3Sdk.ApiClient.instance
21
+const apiKey = defaultClient.authentications['api-key']
22
+apiKey.apiKey = process.env.BREVO_KEY
23
+const apiInstance = new SibApiV3Sdk.TransactionalEmailsApi()
24
+
25
+// Existing activeSession to test against (should not match)
26
+const activeSessions = {
27
+    'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk': {
28
+        email: 'test@testemail.com',
29
+        name: 'john_doe',
30
+        seeking: 'position',
31
+        sessionToken: 'efasdf;laksdfja;lkdjfa;lkdjf',
32
+        expiration: Date.now() + 600000,
33
+        emailWasRespondedTo: false,
34
+        accessToken: null,
35
+    },
36
+}
37
+
38
+let hashedSessionToken = ''
39
+
40
+const payload = {
41
+    email: email,
42
+    name: 'fk',
43
+    seeking: 'seeker',
44
+    sessionToken: 'a;slkdjfa;lskdf;asjkdfl;asdf;klj',
45
+}
46
+
47
+const userCredentials = payload
48
+
49
+const pathToTest = {
50
+    method: 'POST',
51
+    url: '/send-email/',
52
+    payload: JSON.stringify(payload),
53
+}
54
+
55
+test('path /send-email should send test transac email', async t => {
56
+    /**
57
+     * Create a new server and register services,
58
+     * models and routes for testing
59
+     */
60
+    const server = Hapi.server()
61
+    server.registrations = {
62
+        'main-app-plugin': {
63
+            options: {},
64
+        },
65
+    }
66
+
67
+    /**
68
+     * Register Services
69
+     */
70
+    await plugin.register(server)
71
+    server.services()['userService'] = new UserService(server)
72
+    server.services()['userService']['activeSessions'] = activeSessions
73
+
74
+    const hashToken = token => {
75
+        const salt = process.env.APP_SESSION_SALT
76
+        try {
77
+            return crypto.createHmac('sha256', salt).update(token).digest('hex')
78
+        } catch (err) {
79
+            throw new Error(err.message)
80
+        }
81
+    }
82
+
83
+    /**
84
+     * Sends a Transactional Email via Brevo
85
+     * @ returns {Object}
86
+     */
87
+    const emailSent = async userCredentials => {
88
+        hashedSessionToken = hashToken(userCredentials.sessionToken)
89
+        if (Object.keys(activeSessions).includes(hashedSessionToken)) {
90
+            return new Error('session already in cache!!')
91
+        }
92
+        // Set expiration time for ten minutes from now
93
+        const duration = 600000
94
+
95
+        activeSessions[hashedSessionToken] = {
96
+            email: userCredentials.email,
97
+            name: userCredentials.name,
98
+            seeking: userCredentials.seeking,
99
+            sessionToken: userCredentials.sessionToken,
100
+            expiration: Date.now() + duration,
101
+            emailWasRespondedTo: false,
102
+            accessToken: null,
103
+        }
104
+        const sendSmtpEmail = {
105
+            to: [
106
+                {
107
+                    email: userCredentials.email,
108
+                },
109
+            ],
110
+            templateId: 1,
111
+            params: {
112
+                // TODO: Change this in production...
113
+                link: `localhost:3000/verify/${hashedSessionToken}`,
114
+            },
115
+        }
116
+        return await apiInstance.sendTransacEmail(sendSmtpEmail).then(
117
+            data => {
118
+                return { wasSuccessfull: true, data: data }
119
+            },
120
+            error => {
121
+                return { wasSuccessfull: false, error: error }
122
+            },
123
+        )
124
+    }
125
+
126
+    hashedSessionToken = Object.keys(activeSessions).find(hashedToken => {
127
+        return activeSessions[`${hashedToken}`].email === userCredentials.email
128
+    })
129
+
130
+    stub(server.services()['userService'], 'emailSent').returns(
131
+        await emailSent(userCredentials),
132
+    )
133
+
134
+    const mockReturn = {
135
+        emailSentSuccessfully: true,
136
+        hashedSessionToken,
137
+    }
138
+
139
+    /**
140
+     * Test the server with registered models and services
141
+     */
142
+    const { payload } = await server.inject(pathToTest)
143
+    const res = JSON.parse(payload)
144
+    t.deepEqual(res.ok, true)
145
+    t.deepEqual(res.data, mockReturn)
146
+    server.stop()
147
+})

+ 101
- 0
backend/tests/user-signup.spec.js 查看文件

@@ -0,0 +1,101 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const UserService = require('../lib/services/user.js')
7
+
8
+const plugin = require('../lib/plugins/user.js')
9
+const User = require('../lib/models/user.js')
10
+const Auth = require('../lib/models/authentication.js')
11
+
12
+const payload = {
13
+    user_name: 'john_doe',
14
+    user_email: 'test@testemail.com',
15
+    is_poster: 1,
16
+    user_pass: 'password',
17
+}
18
+
19
+const mockInsert = {
20
+    ...payload,
21
+    id: 15,
22
+    is_admin: 0,
23
+    is_verified: 1,
24
+}
25
+
26
+const mockReturn = {
27
+    is_admin: 0,
28
+    is_poster: 1,
29
+    is_verified: 1,
30
+    user_email: 'test@testemail.com',
31
+    user_id: 15,
32
+    user_name: 'john_doe',
33
+}
34
+
35
+const pathToTest = {
36
+    method: 'POST',
37
+    url: '/signup',
38
+    payload: JSON.stringify(payload),
39
+}
40
+
41
+test('path /signup should return new user info', async t => {
42
+    /**
43
+     * Create a new server and register services,
44
+     * models and routes for testing
45
+     * -
46
+     * NOTE: We use a mocked registerModel() and register
47
+     * models manually. Normally this is handled by
48
+     * Schwifty at runtime.
49
+     */
50
+    const server = Hapi.server()
51
+    /**
52
+     * Overload so we don't register any models
53
+     * using the plugin call (see plugins/profile.js)
54
+     * and Manually load the model we need for the test
55
+     */
56
+    server.registerModel = () => {}
57
+    server.models = () => ({ User, Auth })
58
+    // TODO: Apply server.registrations to other test specs
59
+    server.registrations = {
60
+        'main-app-plugin': {
61
+            options: {},
62
+        },
63
+    }
64
+
65
+    /**
66
+     * Register Routes and Services as usual
67
+     */
68
+    await plugin.register(server)
69
+    server.services()['userService'] = new UserService(server)
70
+
71
+    /**
72
+     * Replace Objection model methods with our own mock functions
73
+     * !: Janky - might be better to temp knex sqlite instance
74
+     */
75
+    stub(server.models()['User'], 'query').returns({
76
+        where: () => {
77
+            if (mockInsert.user_email !== 'test@testemail.com') {
78
+                return [mockInsert.user_email]
79
+            } else return []
80
+        },
81
+        insert: () => mockInsert,
82
+    })
83
+
84
+    stub(server.models()['Auth'], 'query').returns({
85
+        insert: () => mockInsert,
86
+        throwIfNotFound: () => ({
87
+            where: () => ({
88
+                patch: () => ({}),
89
+            }),
90
+        }),
91
+    })
92
+
93
+    /**
94
+     * Test the server with registered models and services
95
+     */
96
+    const { payload } = await server.inject(pathToTest)
97
+    const res = JSON.parse(payload)
98
+    t.deepEqual(res.ok, true)
99
+    t.deepEqual(res.data, mockReturn)
100
+    server.stop()
101
+})

+ 173
- 0
backend/tests/user-validate-session.spec.js 查看文件

@@ -0,0 +1,173 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const { stub } = require('sinon')
5
+const Hapi = require('@hapi/hapi')
6
+const UserService = require('../lib/services/user.js')
7
+const ProfileService = require('../lib/services/profile/index.js')
8
+
9
+const plugin = require('../lib/plugins/user.js')
10
+const JWT = require('jsonwebtoken')
11
+const User = require('../lib/models/user.js')
12
+const Profile = require('../lib/models/profile.js')
13
+
14
+// Dummy Method So JWT can be verified
15
+const createToken = (data, expiration = 600) => {
16
+    const key = process.env.APP_SECRET
17
+    const obj = {}
18
+
19
+    Object.assign(obj, { ...data })
20
+    return JWT.sign(obj, key, { expiresIn: expiration })
21
+}
22
+
23
+// Dummy Data
24
+const payload = 'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk'
25
+const email = 'test@testemail.com'
26
+
27
+const userData = {
28
+    email,
29
+    name: 'fk',
30
+    seeking: 'position',
31
+    sessionToken: createToken(this),
32
+}
33
+
34
+const userInDb = {
35
+    user_id: 101,
36
+    user_name: 'john_doe',
37
+    user_email: email,
38
+    is_admin: 0,
39
+    is_poster: 0,
40
+    is_verified: 0,
41
+}
42
+
43
+const allProfiles = [
44
+    {
45
+        profile_id: 147,
46
+        user_id: 101,
47
+    },
48
+]
49
+
50
+// Existing activeSession
51
+const activeSessions = {
52
+    'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk': {
53
+        email,
54
+        name: 'john_doe',
55
+        seeking: 'position',
56
+        sessionToken: userData.sessionToken,
57
+        expiration: Date.now() + 600000,
58
+        emailWasRespondedTo: true,
59
+        accessToken: null,
60
+    },
61
+}
62
+
63
+const mockReturn = {
64
+    profileId: allProfiles[0].profile_id,
65
+    sessionToken: userData.sessionToken,
66
+}
67
+
68
+const pathToTest = {
69
+    method: 'POST',
70
+    url: '/validate-session',
71
+    payload: JSON.stringify(payload),
72
+}
73
+
74
+test('path /validate-session should return validated session data and profileId', async t => {
75
+    /**
76
+     * Create a new server and register services,
77
+     * models and routes for testing
78
+     * -
79
+     * NOTE: We use register models manually.
80
+     * Normally this is handled by
81
+     * Schwifty at runtime.
82
+     */
83
+    const server = Hapi.server()
84
+
85
+    /**
86
+     * Register Routes and Services as usual
87
+     */
88
+    server.registerModel = () => {}
89
+
90
+    server.models = () => ({
91
+        User,
92
+        Profile,
93
+    })
94
+
95
+    server.registrations = {
96
+        'main-app-plugin': {
97
+            options: {},
98
+        },
99
+    }
100
+
101
+    server.registrations['main-app-plugin'].options.jwtKey = {
102
+        $filter: 'NODE_ENV',
103
+        $default: {
104
+            $param: 'APP_SECRET',
105
+            $default: 'app-secret',
106
+        },
107
+        // Use .env file in production
108
+        production: {
109
+            $param: 'APP_SECRET',
110
+        },
111
+    }
112
+
113
+    await plugin.register(server)
114
+    server.models = () => ({
115
+        User,
116
+        Profile,
117
+    })
118
+
119
+    server.services()['userService'] = new UserService(server)
120
+    server.services()['userService']['activeSessions'] = activeSessions
121
+
122
+    server.services()['profileService'] = new ProfileService(server)
123
+    server.services()['profileService']['_setTagLookup'] = () => {}
124
+
125
+    stub(server.models()['User'], 'query').returns({
126
+        throwIfNotFound: () => ({
127
+            first: () => ({
128
+                where: () => {
129
+                    if (userData.email === userInDb.user_email) {
130
+                        return userInDb
131
+                    }
132
+                },
133
+            }),
134
+        }),
135
+    })
136
+
137
+    stub(server.models()['Profile'], 'query').returns({
138
+        where: () => {
139
+            return [allProfiles.find(obj => obj.user_id === userInDb.user_id)]
140
+        },
141
+        whereIn: () => ({
142
+            withGraphFetched: () => ({
143
+                withGraphFetched: () => ({
144
+                    withGraphFetched: () => [
145
+                        {
146
+                            profile_id: 147,
147
+                            user_id: 101,
148
+                            tags: [],
149
+                            responses: [],
150
+                            user: {
151
+                                user_id: 101,
152
+                                user_name: 'fk',
153
+                                user_email: email,
154
+                                is_admin: 0,
155
+                                is_poster: 0,
156
+                                is_verified: 0,
157
+                            },
158
+                        },
159
+                    ],
160
+                }),
161
+            }),
162
+        }),
163
+    })
164
+
165
+    /**
166
+     * Test the server with registered models and services
167
+     */
168
+    const { payload } = await server.inject(pathToTest)
169
+    const res = JSON.parse(payload)
170
+    t.deepEqual(res.ok, true)
171
+    t.deepEqual(res.data, mockReturn)
172
+    server.stop()
173
+})

+ 70
- 0
backend/tests/user-verify-session.spec.js 查看文件

@@ -0,0 +1,70 @@
1
+'use strict'
2
+
3
+const test = require('ava')
4
+const Hapi = require('@hapi/hapi')
5
+const UserService = require('../lib/services/user.js')
6
+
7
+const plugin = require('../lib/plugins/user.js')
8
+
9
+const params = {
10
+    hashedSessionToken: 'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk',
11
+    // Replace to get hashToMatchNotFound
12
+    // hashedSessionToken : 'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk1'
13
+}
14
+
15
+// Existing activeSession to test against
16
+const activeSessions = {
17
+    'a;lsdkfja;ldfjka;ldfja;lskjdfa;dfjk': {
18
+        email: 'test@testemail.com',
19
+        name: 'john_doe',
20
+        seeking: 'position',
21
+        sessionToken: 'efasdf;laksdfja;lkdjfa;lkdjf',
22
+        expiration: Date.now() + 600000,
23
+        // Should return error took too long to respond to email
24
+        // expiration: 600000,
25
+        emailWasRespondedTo: false,
26
+        accessToken: null,
27
+    },
28
+}
29
+
30
+const mockReturn = {
31
+    hashesMatch: true,
32
+}
33
+
34
+const pathToTest = {
35
+    method: 'GET',
36
+    url: `/verify/${params.hashedSessionToken}`,
37
+}
38
+
39
+test('path /verify/<hashedSessionToken> should return OK on GET', async t => {
40
+    /**
41
+     * Create a new server and register services,
42
+     * models and routes for testing
43
+     */
44
+    const server = Hapi.server()
45
+    server.registrations = {
46
+        'main-app-plugin': {
47
+            options: {},
48
+        },
49
+    }
50
+
51
+    /**
52
+     * Register Routes and Services as usual
53
+     */
54
+    await plugin.register(server)
55
+    server.services()['userService'] = new UserService(server)
56
+    server.services()['userService']['activeSessions'] = activeSessions
57
+
58
+    /**
59
+     * Test the server with registered models and services
60
+     */
61
+    const { payload } = await server.inject(pathToTest)
62
+    const res = JSON.parse(payload)
63
+    t.deepEqual(res.ok, true)
64
+    t.deepEqual(
65
+        activeSessions[params.hashedSessionToken].emailWasRespondedTo,
66
+        true,
67
+    )
68
+    t.deepEqual(res.data, mockReturn)
69
+    server.stop()
70
+})

+ 1
- 2
frontend/src/App.vue 查看文件

@@ -13,7 +13,7 @@ import 'wave-ui/dist/wave-ui.css'
13 13
 import SideBar from './components/SideBar.vue'
14 14
 import TopNav from './components/TopNav.vue'
15 15
 
16
-import { currentProfile } from './services'
16
+import { currentProfile, authenticator } from './services'
17 17
 import { surveyFactory } from './utils'
18 18
 
19 19
 const DEV_MODE = import.meta.env.VITE_DEV == 'true'
@@ -58,7 +58,6 @@ export default {
58 58
             if (currentProfile.isLoggedIn) {
59 59
                 currentProfile.logout()
60 60
             }
61
-
62 61
             await currentProfile.login(profileId, this.$waveui.notify)
63 62
             console.log('---')
64 63
 

+ 3
- 13
frontend/src/components/AspectBar.vue 查看文件

@@ -1,12 +1,8 @@
1 1
 <template lang="pug">
2 2
 figure.w-flex.column
3 3
     figcaption.w-flex.xs12.justify-space-between.align-center
4
-        p(
5
-            :class='{ main: index === 1 }'
6
-            :key='label'
7
-            v-for='(label, index) in labels'
8
-        ) {{ label }}
9
-    w-progress.mb7(round size='0.5em' v-model='progress')
4
+        p(v-for="(label, index) in labels" :key="label" :class="{ 'main': index === 1 }")  {{ label }}
5
+    w-progress(:model-value="percentage" size="0.5em" round).mb7
10 6
 </template>
11 7
 
12 8
 <script>
@@ -16,14 +12,9 @@ export default {
16 12
             required: true,
17 13
             type: Array,
18 14
         },
19
-        percentage: {
20
-            required: false,
21
-            type: Number,
22
-            default: 50,
23
-        },
24 15
     },
25 16
     data: () => ({
26
-        progress: props.percentage,
17
+        percentage: 50,
27 18
     }),
28 19
 }
29 20
 </script>
@@ -37,7 +28,6 @@ figure
37 28
             font-weight: bold
38 29
             text-transform: uppercase
39 30
 
40
-
41 31
     .w-progress
42 32
         background-color: #4C5264
43 33
         .w-progress__progress

+ 4
- 3
frontend/src/components/MainNav.vue 查看文件

@@ -19,6 +19,8 @@ w-toolbar.mt6.py1(bottom fixed)
19 19
         w-button.pa8(disabled)
20 20
             w-icon.mr1.icon-cog(xl :class="{ 'icon-selected': $route.path === '/settings' }")
21 21
             //- p.text-upper settings
22
+    w-button.pa4(v-if="!$route.params.pid" bg-color='transparent' @click="$emit('log-out')")
23
+            w-icon.mr1.icon-lock(xl)
22 24
 </template>
23 25
 
24 26
 <script>
@@ -40,11 +42,10 @@ export default {
40 42
 
41 43
     .w-button
42 44
         background-color: $dark-grey
43
-    
45
+
44 46
     .icon-selected
45 47
         color: $light-green
46
-    
48
+
47 49
     a
48 50
         color:$light-grey
49 51
 </style>
50
-

+ 39
- 13
frontend/src/components/onboarding/Auth.vue 查看文件

@@ -5,9 +5,10 @@
5 5
 </template>
6 6
 
7 7
 <script>
8
-import { Authenticator } from '../../services/auth.service.js'
8
+import { authenticator } from '../../services/auth.service.js'
9 9
 import { createProfileForUserId } from '../../services/profile.service'
10 10
 import { signupUser } from '../../services/user.service.js'
11
+import { scoreSurveyByProfileId } from '@/services'
11 12
 
12 13
 export default {
13 14
     name: 'Auth',
@@ -32,12 +33,8 @@ export default {
32 33
         },
33 34
     },
34 35
     emits: ['update-answers'],
35
-    data: () => ({
36
-        authenticator: {},
37
-    }),
38 36
     async created() {
39 37
         // Establishes New User And Sends Auth Email
40
-        this.authenticator = new Authenticator()
41 38
         try {
42 39
             this.doesUserHaveMinResponses(this.responses)
43 40
             const userPass = this.responses.find(
@@ -48,15 +45,14 @@ export default {
48 45
                 password: userPass.val,
49 46
             })
50 47
             await this.createProfileForNewUser(newUserId, this.responses)
51
-            const accessToken = await this.getAccessToken({
48
+            const sessionToken = await this.createToken({
52 49
                 ...this.answered,
53 50
             })
54
-            console.log('accessToken :=>', accessToken)
55
-            const sessionInfo = await this.authenticator.sendAuthEmail({
51
+            const sessionInfo = await authenticator.sendEmail({
56 52
                 ...this.answered,
57
-                accessToken: accessToken,
53
+                sessionToken: sessionToken,
58 54
             })
59
-            document.cookie = `siimee_access=${sessionInfo.hashedAccessToken}; max-age=600; path=/; secure`
55
+            document.cookie = `siimee_session=${sessionInfo.hashedSessionToken}; max-age=600; path=/`
60 56
         } catch (err) {
61 57
             // TODO: render an error page in this component displaying which
62 58
             // error occurred and how to reach out to staff
@@ -70,8 +66,8 @@ export default {
70 66
                     'User has not answered minimum amount of questions to create profile',
71 67
                 )
72 68
         },
73
-        async getAccessToken(payload) {
74
-            return await this.authenticator.getAccessToken({
69
+        async createToken(payload) {
70
+            return await authenticator.createToken({
75 71
                 payload,
76 72
             })
77 73
         },
@@ -86,7 +82,37 @@ export default {
86 82
         },
87 83
         async createProfileForNewUser(userId, responses) {
88 84
             try {
89
-                await createProfileForUserId(userId, responses)
85
+                const newProfileForNewUser = await createProfileForUserId(
86
+                    userId,
87
+                    responses,
88
+                )
89
+                if (!newProfileForNewUser)
90
+                    throw Error(
91
+                        `ERROR: Unable to create newProfile for userId: ${userId}`,
92
+                    )
93
+
94
+                // NOTE: Populates matchQueue table with bare bone
95
+                // minimum filtered results of possible matches
96
+                // Based off of initial survey answers
97
+                const createdProfileId = newProfileForNewUser.profile_id
98
+                const maxDistance = this.responses
99
+                    .find(response => {
100
+                        return response.response_key_id === 19
101
+                    })
102
+                    .val.split(' ')[0]
103
+                const presence = this.responses.find(response => {
104
+                    return response.response_key_id === 15
105
+                }).val
106
+
107
+                // NOTE: duration hard coded for now as bare bones
108
+                // survey doesn't have duration as one of its steps...
109
+                const duration = 'full-time'
110
+                await scoreSurveyByProfileId(
111
+                    createdProfileId,
112
+                    maxDistance,
113
+                    duration,
114
+                    presence,
115
+                )
90 116
             } catch (err) {
91 117
                 throw new Error(err)
92 118
             }

+ 7
- 2
frontend/src/components/onboarding/QuestionResponse.vue 查看文件

@@ -28,9 +28,10 @@ export default {
28 28
             type: Number,
29 29
             required: true,
30 30
         },
31
-        surveyStepsCount: {
32
-            type: Number,
31
+        survey: {
32
+            type: Object,
33 33
             required: true,
34
+            default: () => {},
34 35
         },
35 36
     },
36 37
     emits: ['update-answers'],
@@ -38,7 +39,11 @@ export default {
38 39
         radioItems: [1, 2, 3, 4, 5],
39 40
         answer: null,
40 41
         noChoiceMade: null,
42
+        surveyStepsCount: null,
41 43
     }),
44
+    created() {
45
+        this.surveyStepsCount = this.survey?.steps.length
46
+    },
42 47
     methods: {
43 48
         onUpdate(index) {
44 49
             this.noChoiceMade = false

+ 0
- 1
frontend/src/entities/profile/profile.js 查看文件

@@ -13,7 +13,6 @@ class Profile extends _baseRecord {
13 13
      */
14 14
     constructor({ email, ...profileData }) {
15 15
         super()
16
-
17 16
         this.type = this.constructor.name.toLowerCase()
18 17
 
19 18
         /**  Fields */

+ 5
- 7
frontend/src/entities/survey/survey.js 查看文件

@@ -40,15 +40,13 @@ class Survey extends _baseRecord {
40 40
     }
41 41
 
42 42
     hasMinResponsesToCreateProfile(responses) {
43
-        const neededResponseKeys = [8, 7, 11, 9]
44
-        const hasNeededResponseKey = responses => {
45
-            return responses.every(response => {
46
-                neededResponseKeys.includes(response.response_key_id)
43
+        const neededResponseKeys = [8, 7, 10, 11, 15, 19, 9]
44
+        const hasMinResponses = () =>
45
+            responses.every(response => {
46
+                return neededResponseKeys.includes(response.response_key_id)
47 47
             })
48
-        }
49
-        return hasNeededResponseKey
48
+        return hasMinResponses()
50 49
     }
51
-
52 50
     validateAnswer(payload) {
53 51
         const { question, input } = payload
54 52
 

+ 22
- 3
frontend/src/router/guards.js 查看文件

@@ -1,4 +1,5 @@
1
-import { currentProfile } from '../services'
1
+import WaveUI from '../../node_modules/wave-ui/src/wave-ui/core'
2
+import { authenticator, currentProfile } from '../services'
2 3
 
3 4
 const DEV_MODE = import.meta.env.VITE_DEV == 'true'
4 5
 
@@ -13,7 +14,25 @@ async function log(to) {
13 14
     }
14 15
 }
15 16
 
16
-const checkLoginStatus = (destination, nextCb) => {
17
+const loginIfToken = async () => {
18
+    const sessionData = await authenticator.verifySessionCookie(
19
+        'siimee_session',
20
+    )
21
+    if (
22
+        sessionData?.profileId &&
23
+        sessionData?.sessionToken &&
24
+        !currentProfile.isLoggedIn
25
+    ) {
26
+        await currentProfile.login(
27
+            sessionData.profileId,
28
+            WaveUI.instance.notify,
29
+            sessionData.sessionToken,
30
+        )
31
+    }
32
+}
33
+
34
+const checkLoginStatus = async (destination, nextCb) => {
35
+    await loginIfToken()
17 36
     log(destination)
18 37
     if (DEV_MODE) {
19 38
         nextCb()
@@ -32,6 +51,6 @@ const checkLoginStatus = (destination, nextCb) => {
32 51
     } else {
33 52
         nextCb()
34 53
     }
35
-}
54
+        }
36 55
 
37 56
 export { checkLoginStatus }

+ 36
- 11
frontend/src/services/auth.service.js 查看文件

@@ -1,21 +1,46 @@
1 1
 import { db } from '../utils/db.js'
2 2
 
3 3
 class Authenticator {
4
-    constructor() {
5
-        this.curentUser = null
4
+    async sendEmail(answered) {
5
+        return await db.post('/user/send-email/', answered)
6 6
     }
7
-    async sendAuthEmail(answered) {
8
-        return await db.post('/user/sendemail/', answered)
9
-    }
10
-    async verifyAuthSession(hashedToken) {
7
+    async verifySession(hashedToken) {
11 8
         return await db.get(`/user/verify/${hashedToken}`)
12 9
     }
13
-    async getAccessToken(req) {
14
-        return await db.post('/user/getaccess', req, true)
10
+    async createToken(req) {
11
+        return await db.post('/user/token', req, true)
12
+    }
13
+    async validateSession(hashedSessionToken) {
14
+        return await db.post('/user/validate-session', hashedSessionToken, true)
15
+    }
16
+    async authenticateLoginCredentials(credentials) {
17
+        return await db.post('/user/login', credentials)
18
+    }
19
+    async removeSession(hashedSessionToken) {
20
+        return await db.post('/user/remove-session', hashedSessionToken, true)
21
+    }
22
+    grabStoredCookie(cookieKey) {
23
+        const cookies = document.cookie.split('; ').reduce((prev, current) => {
24
+            const [name, ...value] = current.split('=')
25
+            prev[name] = value.join('=')
26
+            return prev
27
+        }, {})
28
+        const cookieVal =
29
+            cookieKey in cookies ? cookies[`${cookieKey}`] : undefined
30
+        return cookieVal
15 31
     }
16
-    async validateSession(hashedAccessToken) {
17
-        return await db.post('/user/validatesession', hashedAccessToken, true)
32
+    async verifySessionCookie(sessionCookie) {
33
+        const hashedAccessToken = this.grabStoredCookie(sessionCookie)
34
+        if (!hashedAccessToken)
35
+            return console.warn('WARNING :=> accessToken is not defined')
36
+        const validatedToken = await this.validateSession(hashedAccessToken)
37
+        if (validatedToken.error) {
38
+            console.error('ERROR :=>', validatedToken.error)
39
+        } else {
40
+            return validatedToken
41
+        }
18 42
     }
19 43
 }
44
+const authenticator = new Authenticator()
20 45
 
21
-export { Authenticator }
46
+export { authenticator, Authenticator }

+ 1
- 0
frontend/src/services/index.js 查看文件

@@ -6,3 +6,4 @@ export * from './queue.service.js'
6 6
 export * from './chat.service.js'
7 7
 export * from './notification.service.js'
8 8
 export * from './login.service.js'
9
+export * from './auth.service.js'

+ 11
- 2
frontend/src/services/login.service.js 查看文件

@@ -77,13 +77,13 @@ class Login {
77 77
      * @param {number} profileId
78 78
      * @returns {number} stored reactive id
79 79
      */
80
-    async login(profileId, cb) {
80
+    async login(profileId, cb, sessionToken = null) {
81 81
         this._loading.value = true
82 82
         // First check if profile exists
83 83
         console.warn('[Login Service warn]: Logging in:', profileId)
84 84
 
85 85
         // TODO: You can probably use this call to get responses, groupings and tags
86
-        this._profile = await fetchProfileByProfileId(profileId)
86
+        this._profile = await fetchProfileByProfileId(profileId, sessionToken)
87 87
         this.id.value = this._profile.profile_id
88 88
 
89 89
         if (!this.id.value) {
@@ -114,13 +114,22 @@ class Login {
114 114
     }
115 115
     logout() {
116 116
         console.warn('[Login Service warn]: Logging out:', this.id.value)
117
+        this._loading.value = true
118
+        this._profile = null
117 119
         this.id.value = null
120
+        this.groupings = []
121
+        this.queue = []
122
+        this.responses = []
123
+        this.tags = []
118 124
         if (this.toaster) {
119 125
             this.toaster.stop()
120 126
         }
121 127
         if (this.chatter) {
122 128
             this.chatter.stop()
123 129
         }
130
+        this.toaster = null
131
+        this.chatter = null
132
+        this._loading.value = false
124 133
     }
125 134
 
126 135
     async getTags() {

+ 2
- 2
frontend/src/services/user.service.js 查看文件

@@ -9,9 +9,9 @@ const signupUser = async user => {
9 9
         user_name: user.name,
10 10
         user_email: user.email,
11 11
         user_pass: user.password,
12
-        is_poster: user.seeking == 'position' ? 0 : 1,
12
+        is_poster: user.seeking === 'position' ? 0 : 1,
13 13
     }
14
-    return await db.post(`/user/signup`, payload)
14
+    return await db.post('/user/signup', payload)
15 15
 }
16 16
 
17 17
 export { signupUser }

+ 1
- 0
frontend/src/utils/index.js 查看文件

@@ -2,6 +2,7 @@ import Joi from 'joi'
2 2
 import { SurveyFactory } from './survey.js'
3 3
 import { possible } from './lang.js'
4 4
 import { pidMixin, profileMixin } from './mixins.js'
5
+import { authenticator } from '../services/auth.service.js'
5 6
 
6 7
 // This will NOT work until ES2022 gets assert in browsers
7 8
 // import config from '../../../backend/db/data-generator/config.json' assert { type: 'json' }

+ 3
- 3
frontend/src/utils/lang.js 查看文件

@@ -34,7 +34,10 @@ const initialSteps = {
34 34
     splash: 'splash',
35 35
     email: 'email',
36 36
     name: 'name',
37
+    zipcode: 'zipcode',
37 38
     seeking: 'seeking',
39
+    presence: 'presence',
40
+    distance: 'distance',
38 41
     password: 'password',
39 42
 }
40 43
 
@@ -45,17 +48,14 @@ const allSteps = {
45 48
         aspect01: 'aspect-1',
46 49
         aspect02: 'aspect-2',
47 50
         aspect03: 'aspect-3',
48
-        zipcode: 'zipcode',
49 51
         urgency: 'urgency',
50 52
         aspect04: 'aspect-4',
51 53
         aspect05: 'aspect-5',
52 54
         aspect06: 'aspect-6',
53
-        presence: 'presence',
54 55
         duration: 'duration',
55 56
         pronouns: 'pronouns',
56 57
         language: 'language',
57 58
         image: 'image',
58
-        distance: 'distance',
59 59
         blurb: 'blurb',
60 60
         // experience: 'experience',
61 61
         // roles: 'role',

+ 1
- 1
frontend/src/utils/survey.js 查看文件

@@ -84,7 +84,7 @@ class SurveyFactory {
84 84
         // Splash page is placed at beginning of survey
85 85
         mutatedResponseKeys.unshift(splash)
86 86
         // Auth page is placed after email/password
87
-        mutatedResponseKeys.splice(5, 0, auth)
87
+        mutatedResponseKeys.splice(8, 0, auth)
88 88
         return mutatedResponseKeys
89 89
     }
90 90
     async getQuestions() {

+ 21
- 2
frontend/src/views/HomeView.vue 查看文件

@@ -1,4 +1,5 @@
1 1
 <template lang="pug">
2
+// TODO: Add router link to OnboardingView.vue's survey
2 3
 main.view--home
3 4
     article.w-flex.sm-column.md-row.align-center
4 5
         template(v-if='isLoading')
@@ -9,7 +10,7 @@ main.view--home
9 10
 
10 11
         template(v-else-if='cards.length === 0')
11 12
             p No profiles in match_queue.
12
-    MainNav
13
+    MainNav(@log-out='logout')
13 14
 </template>
14 15
 
15 16
 <script>
@@ -21,7 +22,11 @@ import PairingButton from '../components/PairingButton.vue'
21 22
 
22 23
 import { Card } from '../entities'
23 24
 
24
-import { currentProfile, fetchQueueByProfileId } from '../services'
25
+import {
26
+    currentProfile,
27
+    authenticator,
28
+    fetchQueueByProfileId,
29
+} from '../services'
25 30
 import { mixins } from '../utils'
26 31
 
27 32
 const notificationOpts = {
@@ -90,6 +95,20 @@ export default {
90 95
             )
91 96
             this.fetchedCards.push(...newQueue) // update fetchedCards => recalculate cards
92 97
         },
98
+        async logout() {
99
+            if (currentProfile.isLoggedIn) {
100
+                currentProfile.logout()
101
+            }
102
+            const hashedSessionToken =
103
+                authenticator.grabStoredCookie('siimee_session')
104
+            const removedSession = await authenticator.removeSession(
105
+                hashedSessionToken,
106
+            )
107
+            if (removedSession.error)
108
+                console.error('ERROR :=>', removedSession.error)
109
+            document.cookie = `siimee_session=''; max-age=0; path=/`
110
+            this.$router.push('/onboarding')
111
+        },
93 112
         // this can be placed in utils/notification.js
94 113
         notify(payload) {
95 114
             notificationOpts.message = payload

+ 59
- 7
frontend/src/views/LoginView.vue 查看文件

@@ -1,21 +1,73 @@
1 1
 <template lang="pug">
2 2
 main.view--login
3
-
4 3
     article.pa12
5
-        form
6
-            w-input.mb4(label="User E-mail" tile outline v-model="form.profileId" inner-icon-left='icon-envelope')
7
-            w-input(label="Password" type="password" tile outline inner-icon-left='icon-eye')
8
-
9
-            //- Emit up an event so we can sync App pid with currentProfile.id
10
-            w-button.xs12.mt12(@click="$emit('updatePid', form.profileId)" type="submit") submit
4
+        div(v-if='emailSentSuccessfully === null')
5
+            form
6
+                w-input.mb4(
7
+                    inner-icon-left='icon-envelope'
8
+                    label='User E-mail'
9
+                    outline
10
+                    tile
11
+                    v-model='form.email'
12
+                )
13
+                w-input(
14
+                    inner-icon-left='icon-eye'
15
+                    label='Password'
16
+                    outline
17
+                    tile
18
+                    type='password'
19
+                    v-model='form.password'
20
+                )
21
+                w-button.xs12.mt12(@click='login') submit
22
+        div(v-else-if='emailSentSuccessfully === false')
23
+            p.verify-message Email Was Not Sent Successfully, please contact your Email Service Provider or Systems Administrator.
24
+        div(v-else)
25
+            p.verify-message Thanks for logging in!
26
+            p.verify-message We'll just need you to verify your email address to continue. Please check your email!
11 27
 </template>
12 28
 
13 29
 <script>
30
+import { authenticator } from '../services'
14 31
 export default {
15 32
     data: () => ({
16 33
         form: {
17 34
             profileId: null,
35
+            email: null,
36
+            password: null,
18 37
         },
38
+        emailSentSuccessfully: null,
19 39
     }),
40
+    methods: {
41
+        async login() {
42
+            const loginCredentials = {
43
+                user_email: this.form.email,
44
+                password: this.form.password,
45
+            }
46
+            const credentials =
47
+                await authenticator.authenticateLoginCredentials(
48
+                    loginCredentials,
49
+                )
50
+            // emailSentSuccessfully: emailSent.wasSuccessfull,
51
+            const sessionInfo = await authenticator.sendEmail({
52
+                ...credentials.answered,
53
+                sessionToken: credentials.jwt,
54
+            })
55
+            if (sessionInfo.emailSentSuccessfully) {
56
+                this.emailSentSuccessfully = true
57
+            }
58
+            document.cookie = `siimee_session=${sessionInfo.hashedSessionToken}; max-age=600; path=/`
59
+        },
60
+    },
20 61
 }
21 62
 </script>
63
+
64
+<style>
65
+.verify-message {
66
+    display: flex;
67
+    justify-content: center;
68
+    text-align: center;
69
+    margin: 0 auto;
70
+    font-weight: 700;
71
+    font-size: 160%;
72
+}
73
+</style>

+ 22
- 41
frontend/src/views/OnboardingView.vue 查看文件

@@ -15,7 +15,6 @@ main.view--onboarding
15 15
                 :question='step'
16 16
                 :responses='responses'
17 17
                 :survey='survey'
18
-                :surveyStepsCount='survey?.steps?.length'
19 18
                 @handle-submit='onSubmit'
20 19
                 @update-answers='updateAnswers'
21 20
                 v-if='step && currentStep == i'
@@ -30,16 +29,14 @@ main.view--onboarding
30 29
             p(v-if='currentStep != 0') You have completed: {{ currentStep }} / {{ survey?.steps?.length }} survey steps
31 30
 
32 31
     article(v-else)
33
-        SurveyCompleteView(:answers='answered' :surveySteps='survey?.steps')
32
+        SurveyCompleteView(:answers='answered' :surveySteps='survey.steps' :responses='responses')
34 33
 </template>
35 34
 
36 35
 <script>
37
-import { Authenticator } from '../services/auth.service.js'
36
+import { currentProfile, authenticator } from '../services'
38 37
 import { surveyFactory } from '@/utils'
39 38
 import stepViews from '@/components/onboarding'
40 39
 import SurveyCompleteView from './SurveyCompleteView.vue'
41
-let hashedAccessToken = null
42
-let currentProfileId = null
43 40
 
44 41
 export default {
45 42
     name: 'OnboardingView',
@@ -54,18 +51,19 @@ export default {
54 51
         currentStep: 0,
55 52
         survey: null,
56 53
         invalidResponse: false,
57
-        authenticator: {},
58 54
     }),
59 55
     async created() {
60 56
         this.survey = await surveyFactory.createSurvey()
61
-        this.authenticator = new Authenticator()
62
-        hashedAccessToken = this.grabStoredCookie('siimee_access')
63 57
         try {
64
-            const sessionData = await this.verifySession(hashedAccessToken)
65
-            currentProfileId = sessionData.profileId
66
-            this.responses = sessionData.responses
67
-            this.currentStep = this.responses.length + 3
68
-            this.goToStep(this.currentStep)
58
+            const sessionData =
59
+                await authenticator.verifySessionCookie('siimee_session')
60
+            if (sessionData) {
61
+                this.responses = this.formatResponses(
62
+                    currentProfile._profile.responses,
63
+                )
64
+                this.currentStep = this.responses.length + 3
65
+                this.goToStep(this.currentStep)
66
+            }
69 67
         } catch (err) {
70 68
             console.error('ERROR :=>', err)
71 69
             this.goToStep(0)
@@ -78,29 +76,13 @@ export default {
78 76
         async goToStep(num) {
79 77
             this.currentStep = num
80 78
         },
81
-        grabStoredCookie(cookieKey) {
82
-            const cookies = document.cookie
83
-                .split('; ')
84
-                .reduce((prev, current) => {
85
-                    const [name, ...value] = current.split('=')
86
-                    prev[name] = value.join('=')
87
-                    return prev
88
-                }, {})
89
-            const cookieVal =
90
-                cookieKey in cookies ? cookies[`${cookieKey}`] : undefined
91
-            return cookieVal
92
-        },
93
-        async verifySession(hashedAccessToken) {
94
-            if (!hashedAccessToken)
95
-                return console.warn('WARNING :=> accessToken is not defined')
96
-            const validatedToken = await this.authenticator.validateSession(
97
-                hashedAccessToken,
98
-            )
99
-            if (validatedToken.error) {
100
-                throw new Error(validatedToken.error)
101
-            } else {
102
-                return validatedToken
103
-            }
79
+        formatResponses(responses) {
80
+            return responses.map(response => {
81
+                return {
82
+                    response_key_id: response.response_key_id,
83
+                    val: response.val,
84
+                }
85
+            })
104 86
         },
105 87
         async updateAnswers(payload) {
106 88
             if (payload) {
@@ -120,15 +102,14 @@ export default {
120 102
                 this.responses.push(response)
121 103
                 if (k === 'aspects') return
122 104
             }
123
-            // NOTE: If user has finished minimum profile creation,
124
-            // Adds survey answers to responses table and verifies tokens on each step
125
-            if (currentProfileId) {
105
+            if (currentProfile._profile?.profile_id) {
126 106
                 await surveyFactory.addNewSurveyAnswer(
127 107
                     this.responses[this.responses.length - 1],
128
-                    currentProfileId,
108
+                    currentProfile._profile.profile_id,
129 109
                 )
110
+                currentProfile._profile.responses = this.responses
130 111
                 try {
131
-                    await this.verifySession(hashedAccessToken)
112
+                    await authenticator.verifySessionCookie('siimee_session')
132 113
                 } catch (err) {
133 114
                     this.currentStep = 0
134 115
                     this.goToStep(this.currentStep)

+ 118
- 65
frontend/src/views/SurveyCompleteView.vue 查看文件

@@ -1,104 +1,151 @@
1 1
 <template lang="pug">
2 2
 main.view--surveycomplete
3
-    article(style='display: flex; flex-direction: column; align-items: center; text-align: center;')
3
+    article(
4
+        style='display: flex; flex-direction: column; align-items: center; text-align: center'
5
+    )
4 6
         h2 Thanks for Completing Our Survey!!
5 7
         h1 Please review your answers and let us know if you need to change anything.
8
+        div(v-for='response in responses')
9
+            p Your {{ response.stage }}:
10
+            p {{ response.val }}
11
+            br
12
+        .survey-spacer
13
+        div(v-for='aspectResponse in aspectResponses')
14
+            p {{ aspectResponse.question }} :
15
+            br
16
+            p {{ aspectResponse.response }}
17
+            br
6 18
         br
7
-        p(v-for='input in formInputs')
8
-            p(v-for='(value, key) in answers')
9
-                p(v-if='input.survey_stage == key && key !== "password"')
10
-                    p Your {{ key }}: {{ value }}
11
-        br
12
-        p(v-for='input in formDropdowns')
13
-            p(v-for='(value, key) in answers')
14
-                p(v-if='input.survey_stage == key')
15
-                    p Your {{ key }}: {{ value }}
16
-        br
17
-        p(v-for='(response, responseIndex) in questionResponses')
18
-            p(v-for='(value, key) in answers')
19
-                p(v-if='response.survey_stage == key') 
20
-                    p Survey Question {{ responseIndex + 1 }}: 
21
-                    p {{ response.response_key_prompt }}
22
-                    p You Answered: {{ value }}
23
-                    br
24
-        w-button.ma1(@click="changeAnswers") Change Answers
25
-        w-button.ma1(@click="finalSubmit") Submit Answers
19
+        // TODO: Bring user to another View.vue page that allows 
20
+        // them to change their answers to the survey
21
+        // OR render radio buttons next to the questions/answers above,
22
+        // allowing them to return to ONLY that specific survey step 
23
+        // using this.$emit('somefunc') back up to OnboardingView.vue
24
+        // Toggling a boolean flag that prevents "SUBMIT" from goToStep(), 
25
+        // and instead brings them back here...
26
+        // 
27
+        // w-button.ma1(@click="changeAnswers") Change Answers
26 28
 </template>
27 29
 
28 30
 <script>
29
-import {
30
-    createProfileForUserId,
31
-    currentProfile,
32
-    signUpUser
33
-} from '@/services'
34
-
31
+import { currentProfile } from '../services'
35 32
 export default {
36 33
     props: {
37
-        answers: {
38
-            type: Object,
39
-            default: () => ({}),
40
-        },
41 34
         surveySteps: {
42 35
             type: Array,
43 36
             default: () => [],
44 37
         },
45 38
     },
46 39
     data: () => ({
47
-        surveyObjects: [],
48
-        formInputs: [],
49
-        questionResponses: [],
50
-        formDropdowns: [],
40
+        responses: {},
41
+        aspectQuestions: {},
42
+        surveyStages: {},
43
+        aspectResponses: [],
51 44
     }),
52 45
     created() {
53
-        this.surveySteps.forEach((step) => {
54
-            switch (step.component) {
55
-                case 'FormInput':
56
-                    this.formInputs.push(step)
57
-                    break
58
-                case 'FormDropdown':
59
-                    this.formDropdowns.push(step)
60
-                    break
61
-                case 'QuestionResponse':
62
-                    this.questionResponses.push(step)
63
-                    break
64
-            }
65
-        })
46
+        this.parseSurvey(this.surveySteps)
47
+        this.aspectResponses = this.grabAspectResponses(
48
+            currentProfile._profile.responses,
49
+            this.aspectQuestions,
50
+        )
51
+        const responses = this.grabResponsesFromProfile(this.aspectQuestions)
52
+        this.responses = this.appendStagesToResponses(
53
+            responses,
54
+            this.surveyStages,
55
+        )
66 56
     },
67 57
     methods: {
68
-        changeAnswers(){
58
+        parseSurvey(surveySteps) {
59
+            surveySteps.forEach(step => {
60
+                const isAspect = step.category === 'aspect'
61
+                if (isAspect) {
62
+                    this.aspectQuestions[`${step.response_key_id}`] =
63
+                        step.response_key_prompt
64
+                } else {
65
+                    this.surveyStages[`${step.response_key_id}`] =
66
+                        step.survey_stage
67
+                }
68
+            })
69
+        },
70
+        grabResponsesFromProfile(aspectQuestions) {
71
+            const aspectQuestionsKeys = Object.keys(aspectQuestions).map(Number)
72
+            const responses = currentProfile._profile.responses
73
+                .map(response => {
74
+                    if (
75
+                        !aspectQuestionsKeys.includes(response.response_key_id)
76
+                    ) {
77
+                        return response
78
+                    }
79
+                })
80
+                .filter(res => {
81
+                    return typeof res === 'object'
82
+                })
83
+            return responses
84
+        },
85
+        appendStagesToResponses(responses, surveyStages) {
86
+            const responsesWithStages = responses.map(response => {
87
+                return {
88
+                    ...response,
89
+                    stage: surveyStages[`${response.response_key_id}`],
90
+                }
91
+            })
92
+            return responsesWithStages
93
+        },
94
+        grabAspectResponses(responses, questions) {
95
+            return responses
96
+                .map(response => {
97
+                    const prompt = questions[`${response.response_key_id}`]
98
+                    if (prompt) {
99
+                        return {
100
+                            question: questions[`${response.response_key_id}`],
101
+                            response: response.val,
102
+                        }
103
+                    }
104
+                })
105
+                .filter(res => {
106
+                    return typeof res === 'object'
107
+                })
108
+        },
109
+        // changeAnswers() {},
110
+    },
111
+    /* 
112
+    NOTE: Incomplete logic for adjusting matchQueue results based 
113
+    off of further completion of survey, commented out for now as 
114
+    to not overwrite methods
115
+    methods: {
116
+        changeAnswers() {
69 117
             console.log('change answers')
70
-            
71 118
         },
72 119
 
73
-        async finalSubmit(){
120
+        async finalSubmit() {
74 121
             // separate user info from responses
75
-            const [user, survey] = this._separateUserInfoFromResponses(this.answers)
122
+            const [user, survey] = this._separateUserInfoFromResponses(
123
+                this.answers,
124
+            )
76 125
 
77 126
             // create user
78 127
             const createdUser = await signUpUser(user)
79 128
             if (!createdUser) return
80 129
 
81
-            // create profile 
82
-            const userProfile = await createProfileForUserId(createdUser.user_id, survey)
83
-            if(!userProfile) return
84
-
85
-            /**
86
-             * Login only after there is a user and
87
-             * that user has a profile and all responses
88
-             */
130
+            // create profile
131
+            const userProfile = await createProfileForUserId(
132
+                createdUser.user_id,
133
+                survey,
134
+            )
135
+            if (!userProfile) return
89 136
             this._setLoginForProfile(userProfile)
90 137
 
91 138
             this.$router.push({ name: 'HomeView' })
92 139
         },
93 140
 
94 141
         // TODO write logic to parse answers
95
-        _separateUserInfoFromResponses(answers){
96
-            return ['','']
142
+        _separateUserInfoFromResponses(answers) {
143
+            return ['', '']
97 144
         },
98 145
 
99
-        async _setLoginForProfile(profile){
146
+        async _setLoginForProfile(profile) {
100 147
             const currentId = currentProfile.login(profile.profile_id)
101
-            if(currentId && profile.responses.length){
148
+            if (currentId && profile.responses.length) {
102 149
                 currentProfile.setResponses(profile.responses)
103 150
             }
104 151
             if (!currentProfile.isComplete) {
@@ -107,7 +154,13 @@ export default {
107 154
                 )
108 155
                 return
109 156
             }
110
-        }
111
-    }
157
+        },
158
+    },
159
+    */
112 160
 }
113 161
 </script>
162
+<style>
163
+.survey-spacer {
164
+    height: 1.25rem;
165
+}
166
+</style>

+ 10
- 36
frontend/src/views/VerifyView.vue 查看文件

@@ -5,62 +5,36 @@
5 5
 </template>
6 6
 
7 7
 <script>
8
-import { Authenticator } from '../services/auth.service.js'
8
+import { currentProfile, authenticator } from '../services'
9 9
 let hash = null
10
-let hashedAccessToken = null
11 10
 export default {
12 11
     name: 'VerifyView',
13
-    data: () => ({
14
-        authenticator: {},
15
-    }),
16 12
     async created() {
17
-        this.authenticator = new Authenticator()
18 13
         hash = this.$route.params.hashedToken
19
-        hashedAccessToken = this.grabCookie('siimee_access')
20 14
         try {
21 15
             this.isHashInUrl(hash)
22
-            await this.doesAccessTokenExist(hashedAccessToken)
23 16
             await this.verifyActiveSession(hash)
24
-            await this.isSessionTokenValid(hash)
17
+            const sessionData =
18
+                await authenticator.verifySessionCookie('siimee_session')
19
+            currentProfile.login(
20
+                sessionData.profileId,
21
+                this.$waveui.notify,
22
+                sessionData.accessToken,
23
+            )
25 24
         } catch (err) {
26 25
             console.error(err)
27 26
         }
28
-        this.$router.push('/onboarding')
27
+        this.$router.push('/')
29 28
     },
30 29
     methods: {
31
-        grabCookie(cookieKey) {
32
-            const cookies = document.cookie
33
-                .split('; ')
34
-                .reduce((prev, current) => {
35
-                    const [name, ...value] = current.split('=')
36
-                    prev[name] = value.join('=')
37
-                    return prev
38
-                }, {})
39
-            return `${cookieKey}` in cookies
40
-                ? cookies[`${cookieKey}`]
41
-                : undefined
42
-        },
43 30
         isHashInUrl(hash) {
44 31
             if (!hash) throw new Error('URL contains no hash!')
45 32
         },
46
-        async doesAccessTokenExist(hashedAccessToken) {
47
-            if (!hashedAccessToken)
48
-                throw new Error('accessToken not in cookie store!')
49
-        },
50 33
         async verifyActiveSession(hashedToken) {
51
-            const sessionData = await this.authenticator.verifyAuthSession(
52
-                hashedToken,
53
-            )
34
+            const sessionData = await authenticator.verifySession(hashedToken)
54 35
             if (!sessionData.hashesMatch)
55 36
                 throw new Error('Hash is not in activeSessions!')
56 37
         },
57
-        async isSessionTokenValid(hash) {
58
-            const sessionTokenIsValid =
59
-                await this.authenticator.validateSession(hash)
60
-            if (sessionTokenIsValid.error) {
61
-                throw new Error(sessionTokenIsValid.error)
62
-            }
63
-        },
64 38
     },
65 39
 }
66 40
 </script>

Loading…
取消
儲存