Просмотр исходного кода

:construction: Starting in on stage 2 of email verification

juan_spike
tomit4 3 лет назад
Родитель
Сommit
d55dc3c810

+ 1
- 1
backend/lib/routes/user/email.js Просмотреть файл

24
             const { userService } = request.server.services()
24
             const { userService } = request.server.services()
25
             const userEmail = request.payload.email
25
             const userEmail = request.payload.email
26
             try {
26
             try {
27
-                const emailSent = userService.emailSent(userEmail)
27
+                const emailSent = await userService.emailSent(userEmail)
28
                 return {
28
                 return {
29
                     ok: true,
29
                     ok: true,
30
                     handler: pluginConfig.handlerType,
30
                     handler: pluginConfig.handlerType,

+ 0
- 1
backend/lib/routes/user/generatejwt.js Просмотреть файл

23
         handler: async function (request, h) {
23
         handler: async function (request, h) {
24
             const { userService } = request.server.services()
24
             const { userService } = request.server.services()
25
             const res = request.payload
25
             const res = request.payload
26
-
27
             const token = await userService.createToken(res)
26
             const token = await userService.createToken(res)
28
             try {
27
             try {
29
                 return {
28
                 return {

+ 26
- 10
backend/lib/routes/user/verifyemail.js Просмотреть файл

14
 
14
 
15
 module.exports = {
15
 module.exports = {
16
     method: 'GET',
16
     method: 'GET',
17
-    path: '/verify/{hash}',
17
+    path: '/verify/{hashedEmail}',
18
     options: {
18
     options: {
19
         ...pluginConfig.docs.get,
19
         ...pluginConfig.docs.get,
20
         tags: ['api'],
20
         tags: ['api'],
22
         cors: true,
22
         cors: true,
23
         handler: async function (request, h) {
23
         handler: async function (request, h) {
24
             const { userService } = request.server.services()
24
             const { userService } = request.server.services()
25
-            const hashToMatch = await userService.hashedEmail
26
-            const hash = request.params.hash
27
-            const hashesMatch = hashToMatch === hash ? true : false
25
+            const hash = request.params.hashedEmail
26
+
28
             try {
27
             try {
29
-                if (hashesMatch) {
30
-                    return {
31
-                        ok: true,
32
-                        handler: pluginConfig.handlerType,
33
-                        data: { hashesMatch: hashesMatch },
34
-                    }
28
+                const hashToMatch = Object.keys(userService.hashedEmails).find(
29
+                    email => {
30
+                        return email === hash
31
+                    },
32
+                )
33
+                const now = Date.now()
34
+                // TODO: convert this back to a date object
35
+                const expiration = userService.hashedEmails[hashToMatch]
36
+                if (now > expiration) {
37
+                    delete userService.hashedEmails[hashToMatch]
38
+                    throw new Error(
39
+                        'you took to long to respond to the email...',
40
+                    )
41
+                }
42
+                if (!hashToMatch) {
43
+                    throw new Error('no record of email in cache')
44
+                }
45
+
46
+                return {
47
+                    ok: true,
48
+                    handler: pluginConfig.handlerType,
49
+                    data: { hashesMatch: hashToMatch === hash },
35
                 }
50
                 }
36
             } catch (err) {
51
             } catch (err) {
52
+                console.log('err :=>', err)
37
                 return {
53
                 return {
38
                     ok: false,
54
                     ok: false,
39
                     handler: pluginConfig.handlerType,
55
                     handler: pluginConfig.handlerType,

+ 11
- 10
backend/lib/services/profile/profiler.js Просмотреть файл

9
     constructor(profile, includeResponses = false, type) {
9
     constructor(profile, includeResponses = false, type) {
10
         this.user_id = profile.user_id // int user_id
10
         this.user_id = profile.user_id // int user_id
11
         this.profile_id = profile.profile_id // int profile_id
11
         this.profile_id = profile.profile_id // int profile_id
12
-        this.user_name = 'hidden_name'// string user_name
12
+        this.user_name = 'hidden_name' // string user_name
13
         this.user_email = 'hidden@email.com'
13
         this.user_email = 'hidden@email.com'
14
         this.responses = []
14
         this.responses = []
15
         this.user_type = type
15
         this.user_type = type
48
                     r => r.response_key_id === prefsKeys[i],
48
                     r => r.response_key_id === prefsKeys[i],
49
                 )
49
                 )
50
             })
50
             })
51
-            this.profile_description = this.responses.find(
52
-                r => r.response_key_id === config.blurbKey,
53
-            ).val
54
-            this.profile_media = this.responses
55
-                .filter(r => r.response_key_id === config.mediaKey)
56
-                .map(r => r.val)
57
-            this.profile_languages = this.responses
58
-                .filter(r => r.response_key_id === config.langKey)
59
-                .map(r => r.val)
51
+            // TODO: uncomment, changed to get Onboarding Auth working...
52
+            // this.profile_description = this.responses.find(
53
+            // r => r.response_key_id === config.blurbKey,
54
+            // ).val
55
+            // this.profile_media = this.responses
56
+            // .filter(r => r.response_key_id === config.mediaKey)
57
+            // .map(r => r.val)
58
+            // this.profile_languages = this.responses
59
+            // .filter(r => r.response_key_id === config.langKey)
60
+            // .map(r => r.val)
60
         }
61
         }
61
     }
62
     }
62
 }
63
 }

+ 18
- 7
backend/lib/services/user.js Просмотреть файл

22
         return undefined
22
         return undefined
23
     }
23
     }
24
 }
24
 }
25
-
25
+// const emailsSent = {}
26
 const hasher = async (pwd, steak) => {
26
 const hasher = async (pwd, steak) => {
27
     const hash = await pwd.hash(steak)
27
     const hash = await pwd.hash(steak)
28
     const result = await pwd.verify(steak, hash)
28
     const result = await pwd.verify(steak, hash)
62
     constructor(...args) {
62
     constructor(...args) {
63
         super(...args)
63
         super(...args)
64
         const pwd = new SecurePassword()
64
         const pwd = new SecurePassword()
65
-        this.hashedEmail = ''
66
-        // this.hashedEmails = []
65
+        // TODO: Invalidate this cache somehow after a certain time period has
66
+        // passed
67
+        this.hashedEmails = {
68
+            // NOTE: key is email hash and value is timestamp in ms
69
+            // abc123456: '123456689',
70
+        }
67
         this.pwd = {
71
         this.pwd = {
68
             hash: Util.promisify(pwd.hash.bind(pwd)),
72
             hash: Util.promisify(pwd.hash.bind(pwd)),
69
             verify: Util.promisify(pwd.verify.bind(pwd)),
73
             verify: Util.promisify(pwd.verify.bind(pwd)),
191
      * @param {User} user
195
      * @param {User} user
192
      * @returns {Token}
196
      * @returns {Token}
193
      */
197
      */
198
+
194
     createToken(user) {
199
     createToken(user) {
195
         const key = this.server.registrations['main-app-plugin'].options.jwtKey
200
         const key = this.server.registrations['main-app-plugin'].options.jwtKey
196
 
201
 
208
             },
213
             },
209
             {
214
             {
210
                 // ttlSec: 4 * 60 * 60, // 7 days
215
                 // ttlSec: 4 * 60 * 60, // 7 days
211
-                ttlSec: 60 * 3, // 3 minutes
216
+                // ttlSec: 60 * 3, // 3 minutes
217
+                ttlSec: user.expiration,
212
             },
218
             },
213
         )
219
         )
214
     }
220
     }
270
      * @ returns {Object}
276
      * @ returns {Object}
271
      */
277
      */
272
     async emailSent(userEmail) {
278
     async emailSent(userEmail) {
279
+        const hashedEmail = await hashEmail(userEmail)
280
+        if (Object.keys(this.hashedEmails).includes(hashedEmail)) {
281
+            return new Error('email address already in cache!!')
282
+        }
283
+        // Set expiration time for five minutes from now
284
+        const duration = 1000 * 60 * 5
285
+        this.hashedEmails[hashedEmail] = Date.now() + duration
273
         const sendSmtpEmail = {
286
         const sendSmtpEmail = {
274
             to: [
287
             to: [
275
                 {
288
                 {
279
             templateId: 1,
292
             templateId: 1,
280
             params: {
293
             params: {
281
                 // TODO: Change this in production...
294
                 // TODO: Change this in production...
282
-                link: `localhost:3000/verify/${await hashEmail(userEmail)}`,
295
+                link: `localhost:3000/verify/${hashedEmail}`,
283
             },
296
             },
284
         }
297
         }
285
 
298
 
297
                 }
310
                 }
298
             },
311
             },
299
         )
312
         )
300
-        this.hashedEmail = hashEmail(userEmail)
301
-        // this.hashedEmails.push(hashEmail(userEmail))
302
     }
313
     }
303
 }
314
 }

+ 7
- 7
backend/package-lock.json Просмотреть файл

12
                 "@hapi/glue": "^8.0.0",
12
                 "@hapi/glue": "^8.0.0",
13
                 "@hapi/hapi": "^20.1.3",
13
                 "@hapi/hapi": "^20.1.3",
14
                 "@hapi/inert": "^6.0.3",
14
                 "@hapi/inert": "^6.0.3",
15
-                "@hapi/jwt": "^2.0.1",
15
+                "@hapi/jwt": "^2.2.0",
16
                 "@hapi/vision": "^6.0.1",
16
                 "@hapi/vision": "^6.0.1",
17
                 "@hapipal/confidence": "^6.0.1",
17
                 "@hapipal/confidence": "^6.0.1",
18
                 "@hapipal/schmervice": "^2.0.0",
18
                 "@hapipal/schmervice": "^2.0.0",
776
             }
776
             }
777
         },
777
         },
778
         "node_modules/@hapi/jwt": {
778
         "node_modules/@hapi/jwt": {
779
-            "version": "2.0.1",
780
-            "resolved": "https://registry.npmjs.org/@hapi/jwt/-/jwt-2.0.1.tgz",
781
-            "integrity": "sha512-6/nX/yOIk9mvs+r72LFhF177yOB4yVv3e0Nqn7cIx2CU+VruBHxMKkHraARXx6oUAtiwNuyhW+trO5QeGm9ESQ==",
779
+            "version": "2.2.0",
780
+            "resolved": "https://registry.npmjs.org/@hapi/jwt/-/jwt-2.2.0.tgz",
781
+            "integrity": "sha512-hOzQ/E0O9XemapjYddGH4ReCG5JEHz62zLeNou4Mt282yx7JknCPTTsnsqkxRE+EPVWNGXGz2E3SDlST80hjMw==",
782
             "dependencies": {
782
             "dependencies": {
783
                 "@hapi/b64": "5.x.x",
783
                 "@hapi/b64": "5.x.x",
784
                 "@hapi/boom": "9.x.x",
784
                 "@hapi/boom": "9.x.x",
9198
             }
9198
             }
9199
         },
9199
         },
9200
         "@hapi/jwt": {
9200
         "@hapi/jwt": {
9201
-            "version": "2.0.1",
9202
-            "resolved": "https://registry.npmjs.org/@hapi/jwt/-/jwt-2.0.1.tgz",
9203
-            "integrity": "sha512-6/nX/yOIk9mvs+r72LFhF177yOB4yVv3e0Nqn7cIx2CU+VruBHxMKkHraARXx6oUAtiwNuyhW+trO5QeGm9ESQ==",
9201
+            "version": "2.2.0",
9202
+            "resolved": "https://registry.npmjs.org/@hapi/jwt/-/jwt-2.2.0.tgz",
9203
+            "integrity": "sha512-hOzQ/E0O9XemapjYddGH4ReCG5JEHz62zLeNou4Mt282yx7JknCPTTsnsqkxRE+EPVWNGXGz2E3SDlST80hjMw==",
9204
             "requires": {
9204
             "requires": {
9205
                 "@hapi/b64": "5.x.x",
9205
                 "@hapi/b64": "5.x.x",
9206
                 "@hapi/boom": "9.x.x",
9206
                 "@hapi/boom": "9.x.x",

+ 1
- 1
backend/package.json Просмотреть файл

20
         "@hapi/glue": "^8.0.0",
20
         "@hapi/glue": "^8.0.0",
21
         "@hapi/hapi": "^20.1.3",
21
         "@hapi/hapi": "^20.1.3",
22
         "@hapi/inert": "^6.0.3",
22
         "@hapi/inert": "^6.0.3",
23
-        "@hapi/jwt": "^2.0.1",
23
+        "@hapi/jwt": "^2.2.0",
24
         "@hapi/vision": "^6.0.1",
24
         "@hapi/vision": "^6.0.1",
25
         "@hapipal/confidence": "^6.0.1",
25
         "@hapipal/confidence": "^6.0.1",
26
         "@hapipal/schmervice": "^2.0.0",
26
         "@hapipal/schmervice": "^2.0.0",

+ 12
- 12
frontend/src/components/onboarding/Auth.vue Просмотреть файл

44
         if (this.survey.hasMinResponsesToCreateProfile(this.answered)) {
44
         if (this.survey.hasMinResponsesToCreateProfile(this.answered)) {
45
             const newUser = await signupUser(this.answered)
45
             const newUser = await signupUser(this.answered)
46
             const newUserId = newUser.user_id
46
             const newUserId = newUser.user_id
47
-            const newProfile = await createProfileForUserId(
48
-                newUserId,
49
-                this.responses,
50
-            )
51
-
52
-            // sets jwt authentication cookie to be used in VerifyView.vue
53
-            // TODO: Instead of having this.answered here, simply pass profile_id?
54
-            const jwt = await this.authenticator.generateJwt(this.answered)
47
+            await createProfileForUserId(newUserId, this.responses)
48
+            const jwt = await this.authenticator.generateJwt({
49
+                ...this.answered,
50
+                expiration: 60 * 10,
51
+            })
55
             document.cookie = `siimee_jwt=${jwt}; path=/verify`
52
             document.cookie = `siimee_jwt=${jwt}; path=/verify`
56
-            // this.$emit('set-pid', newProfile.profile_id)
57
-
58
-            // sends authentication email
59
-            this.authenticator.sendAuthEmail(this.answered)
60
         }
53
         }
54
+
55
+        // sets jwt authentication cookie to be used in VerifyView.vue and
56
+        // OnboardingView.vue
57
+        // TODO: Instead of having this.answered here, simply pass profile_id?
58
+
59
+        // sends authentication email
60
+        await this.authenticator.sendAuthEmail(this.answered)
61
     },
61
     },
62
     methods: {
62
     methods: {
63
         // TODO: remove test button above and use a watcher instead to emit this
63
         // TODO: remove test button above and use a watcher instead to emit this

+ 4
- 4
frontend/src/services/auth.service.js Просмотреть файл

5
         this.curentUser = null
5
         this.curentUser = null
6
     }
6
     }
7
     async sendAuthEmail(answered) {
7
     async sendAuthEmail(answered) {
8
-        const emailWasSent = await db.post(`/user/sendemail/`, answered)
9
-        console.log('emailwasSent :=>', emailWasSent)
8
+        const emailWasSent = await db.post('/user/sendemail/', answered)
9
+        return emailWasSent
10
     }
10
     }
11
     // NOTE: these might be better suited as POST requests
11
     // NOTE: these might be better suited as POST requests
12
-    async verifyAuthEmail(hash) {
13
-        const isVerified = await db.get(`/user/verify/${hash}`)
12
+    async verifyAuthEmail(hashedEmail) {
13
+        const isVerified = await db.get(`/user/verify/${hashedEmail}`)
14
         return isVerified.hashesMatch
14
         return isVerified.hashesMatch
15
     }
15
     }
16
     // TODO: this needs to generate the JWT with the RAW email
16
     // TODO: this needs to generate the JWT with the RAW email

+ 0
- 2
frontend/src/services/login.service.js Просмотреть файл

51
      */
51
      */
52
     get isComplete() {
52
     get isComplete() {
53
         // TODO: remove once Vue Router guards allow to redirect via url
53
         // TODO: remove once Vue Router guards allow to redirect via url
54
-        console.log('this.responses :=>', this.responses)
55
-        console.log('surveyFactory.questionsFromDb :=>', surveyFactory.questionsFromDb)
56
         return (
54
         return (
57
             this.responses.length == surveyFactory.questionsFromDb.length &&
55
             this.responses.length == surveyFactory.questionsFromDb.length &&
58
             surveyFactory.questionsFromDb.length > 0
56
             surveyFactory.questionsFromDb.length > 0

+ 48
- 27
frontend/src/views/OnboardingView.vue Просмотреть файл

15
                 :responses='responses'
15
                 :responses='responses'
16
                 :survey='survey'
16
                 :survey='survey'
17
                 :currentStep='currentStep'
17
                 :currentStep='currentStep'
18
-                :jwt='jwt'
19
                 :surveyStepsCount='survey.steps.length'
18
                 :surveyStepsCount='survey.steps.length'
20
                 @handle-submit='onSubmit'
19
                 @handle-submit='onSubmit'
21
                 @update-answers='updateAnswers'
20
                 @update-answers='updateAnswers'
60
         invalidResponse: false,
59
         invalidResponse: false,
61
         // TODO: CURRENTLY WORKING ON**
60
         // TODO: CURRENTLY WORKING ON**
62
         authenticator: {},
61
         authenticator: {},
63
-        jwt: '',
62
+        sessionToken: '',
63
+        accessToken: '',
64
     }),
64
     }),
65
     computed: {
65
     computed: {
66
         // NOTE: perhaps start this off as returning nothing
66
         // NOTE: perhaps start this off as returning nothing
68
         profile: () => currentProfile,
68
         profile: () => currentProfile,
69
     },
69
     },
70
     async created() {
70
     async created() {
71
-        // console.log('this.profile.id :=>', this.profile.id)
71
+        this.survey = await surveyFactory.createSurvey()
72
+        this.authenticator = new Authenticator()
73
+
72
         if (document.cookie.length) {
74
         if (document.cookie.length) {
73
             // TODO: Remove this this.answered section when:
75
             // TODO: Remove this this.answered section when:
74
             // SurveyCompleteView calls all responses from db
76
             // SurveyCompleteView calls all responses from db
77
+            // BUG: NEEDS BROWSER REFRESH TO BE HIT
75
             const siimeeAnswers = JSON.parse(
78
             const siimeeAnswers = JSON.parse(
76
                 this.grabCookie(document.cookie, 'siimee_answered'),
79
                 this.grabCookie(document.cookie, 'siimee_answered'),
77
             )
80
             )
78
-            this.answered = {
79
-                name: siimeeAnswers.name,
80
-                email: siimeeAnswers.email,
81
-                seeking: siimeeAnswers.seeking,
82
-            }
83
-            // TODO: Instead, login using siimee_profile_id from cookie
84
-            const siimeeToken = this.grabCookie(document.cookie, 'siimee_jwt')
85
-            this.jwt = siimeeToken ? siimeeToken : ''
86
-        }
87
-
88
-        this.survey = await surveyFactory.createSurvey()
89
-        this.authenticator = new Authenticator()
90
-        if (this.jwt.length) {
91
-            // TODO: remove once currentProfile is established and user is logged in...
92
-            const jwt = await this.authenticator.validateJwt(this.jwt)
93
-            if (jwt.isValid) {
94
-                // grab profileId from jwt payload and login here
95
-                // use this profile to grab all responses in
96
-                // SurveyCompleteView.vue
81
+            this.sessionToken = this.grabCookie(
82
+                document.cookie,
83
+                'siimee_session',
84
+            )
85
+            const sessionTokenIsValid = await this.authenticator.validateJwt(
86
+                this.sessionToken,
87
+            )
88
+            this.accessToken = this.grabCookie(document.cookie, 'siimee_access')
89
+            if (sessionTokenIsValid.isValid) {
90
+                this.answered = {
91
+                    name: siimeeAnswers.name,
92
+                    email: siimeeAnswers.email,
93
+                    seeking: siimeeAnswers.seeking,
94
+                }
97
                 this.goToStep(6)
95
                 this.goToStep(6)
98
-            } else {
99
-                this.goToStep(0)
100
-                this.jwt = ''
101
-            }
96
+            } else this.goToStep(0)
102
         }
97
         }
103
     },
98
     },
104
     methods: {
99
     methods: {
105
         onSubmit() {
100
         onSubmit() {
106
             console.log(JSON.stringify(this.answered))
101
             console.log(JSON.stringify(this.answered))
107
         },
102
         },
108
-        goToStep(num) {
103
+        async goToStep(num) {
104
+            if (num > 6) {
105
+                this.validateAccessToken()
106
+            }
109
             this.currentStep = num
107
             this.currentStep = num
110
         },
108
         },
109
+        async validateAccessToken() {
110
+            const validatedAccessToken = await this.authenticator.validateJwt(
111
+                this.accessToken,
112
+            )
113
+            if (!validatedAccessToken.isValid) {
114
+                const sessionTokenIsValid = await this.validateSessionToken()
115
+                if (!sessionTokenIsValid) {
116
+                    this.goToStep(0)
117
+                }
118
+            }
119
+        },
120
+        async validateSessionToken() {
121
+            const validatedSessionToken = await this.authenticator.validateJwt(
122
+                this.sessionToken,
123
+            )
124
+            if (validatedSessionToken.isValid) {
125
+                this.accessToken = await this.authenticator.generateJwt({
126
+                    ...this.answered,
127
+                    expiration: 60 * 3,
128
+                })
129
+                return true
130
+            } else return false
131
+        },
111
         grabCookie(cookieString, cookieKey) {
132
         grabCookie(cookieString, cookieKey) {
112
             const cookies = cookieString.split('; ').reduce((prev, current) => {
133
             const cookies = cookieString.split('; ').reduce((prev, current) => {
113
                 const [name, ...value] = current.split('=')
134
                 const [name, ...value] = current.split('=')

+ 21
- 9
frontend/src/views/VerifyView.vue Просмотреть файл

10
     name: 'VerifyView',
10
     name: 'VerifyView',
11
     data: () => ({
11
     data: () => ({
12
         authenticator: {},
12
         authenticator: {},
13
+        answers: {},
13
     }),
14
     }),
14
     async created() {
15
     async created() {
15
         this.authenticator = new Authenticator()
16
         this.authenticator = new Authenticator()
16
         const hashEmail = this.$route.params.email
17
         const hashEmail = this.$route.params.email
18
+
19
+        // TODO: generate a token on the backend here and have it sent over in
20
+        // the headers intead of setting it directly with document.cookie
21
+
17
         const hashesMatch = await this.authenticator.verifyAuthEmail(hashEmail)
22
         const hashesMatch = await this.authenticator.verifyAuthEmail(hashEmail)
18
         const siimeeToken = this.grabToken(document.cookie)
23
         const siimeeToken = this.grabToken(document.cookie)
19
 
24
 
20
-        // TODO: remove once currentProfile is established and user is logged in...
21
-        // NOTE: another siimee_jwt cookie for onboarding path instead of verify
22
-        document.cookie = `siimee_jwt=${siimeeToken}; path=/onboarding`
23
-
25
+        // TODO: Then send this token and receive a different token
24
         const jwt = await this.authenticator.validateJwt(siimeeToken)
26
         const jwt = await this.authenticator.validateJwt(siimeeToken)
25
-
27
+        this.answers = jwt.payload
28
+        const accessToken = await this.generateAccessToken()
26
         if (jwt.isValid && hashesMatch) {
29
         if (jwt.isValid && hashesMatch) {
27
-            // TODO: set jwt.payload.profile_id as siimee_profile_id
28
-            // remove answers, will query db at SurveyCompleteView.vue
29
-            const siimeeAnswers = JSON.stringify(jwt.payload)
30
-            document.cookie = `siimee_answered=${siimeeAnswers} ; path=/onboarding`
30
+            // TODO: establish session access token cookie here
31
+            const siimeeAnswers = JSON.stringify(this.answers)
32
+            document.cookie = `siimee_answered=${siimeeAnswers} ; path=/onboarding; secure`
33
+            // TODO: for session/access cookies, make sure to set http only flag and expiration
34
+            document.cookie = `siimee_session=${siimeeToken} ; path=/onboarding; secure`
35
+            document.cookie = `siimee_access=${accessToken}; path=/onboarding; secure`
31
             this.$router.push('/onboarding')
36
             this.$router.push('/onboarding')
32
         }
37
         }
33
         // else {
38
         // else {
42
             }, {})
47
             }, {})
43
             return 'siimee_jwt' in cookies ? cookies['siimee_jwt'] : undefined
48
             return 'siimee_jwt' in cookies ? cookies['siimee_jwt'] : undefined
44
         },
49
         },
50
+        async generateAccessToken() {
51
+            const accessJwt = await this.authenticator.generateJwt({
52
+                ...this.answers,
53
+                expiration: 60 * 3, // testing for now... extend to 1 hour?
54
+            })
55
+            return accessJwt
56
+        },
45
     },
57
     },
46
 }
58
 }
47
 </script>
59
 </script>

Загрузка…
Отмена
Сохранить