fix(user): race condition during email confirmation (#1575)
This commit is contained in:
parent
5c4f6dab6b
commit
2d37c0fede
|
|
@ -0,0 +1,149 @@
|
|||
import {UserFactory} from '../../factories/user'
|
||||
import {TokenFactory} from '../../factories/token'
|
||||
|
||||
context('Email Confirmation', () => {
|
||||
let user
|
||||
let confirmationToken
|
||||
|
||||
beforeEach(() => {
|
||||
UserFactory.truncate()
|
||||
TokenFactory.truncate()
|
||||
|
||||
// Create a user with status = 1 (StatusEmailConfirmationRequired)
|
||||
user = UserFactory.create(1, {
|
||||
username: 'unconfirmeduser',
|
||||
email: 'unconfirmed@example.com',
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||
status: 1, // StatusEmailConfirmationRequired
|
||||
})[0]
|
||||
|
||||
// Create an email confirmation token for this user
|
||||
// kind: 2 = TokenEmailConfirm
|
||||
confirmationToken = 'test-email-confirm-token-12345678901234567890123456789012'
|
||||
TokenFactory.create(1, {
|
||||
user_id: user.id,
|
||||
kind: 2,
|
||||
token: confirmationToken,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail login before email is confirmed', () => {
|
||||
cy.visit('/login')
|
||||
cy.get('input[id=username]').type(user.username)
|
||||
cy.get('input[id=password]').type('1234')
|
||||
cy.get('.button').contains('Login').click()
|
||||
|
||||
cy.get('div.message.danger').contains('Email address of the user not confirmed')
|
||||
})
|
||||
|
||||
it('Should confirm email and allow login', () => {
|
||||
// Intercept the confirmation API call
|
||||
cy.intercept('POST', '**/user/confirm').as('confirmEmail')
|
||||
|
||||
// Manually set the token in localStorage before visiting the page
|
||||
// This simulates what happens when the user clicks the email link
|
||||
cy.visit('/login', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('emailConfirmToken', confirmationToken)
|
||||
},
|
||||
})
|
||||
|
||||
// Wait for the confirmation API call to complete
|
||||
cy.wait('@confirmEmail', {timeout: 10000}).its('response.statusCode').should('eq', 200)
|
||||
|
||||
// Should show success message
|
||||
cy.get('.message.success', {timeout: 10000}).should('be.visible')
|
||||
cy.get('.message.success').contains('You successfully confirmed your email')
|
||||
|
||||
// Now login should work
|
||||
cy.get('input[id=username]').type(user.username)
|
||||
cy.get('input[id=password]').type('1234')
|
||||
cy.get('.button').contains('Login').click()
|
||||
|
||||
// Should successfully log in
|
||||
cy.url().should('include', '/')
|
||||
cy.url().should('not.include', '/login')
|
||||
// Check that the username appears in the greeting
|
||||
cy.contains(user.username)
|
||||
})
|
||||
|
||||
it('Should fail with invalid confirmation token', () => {
|
||||
// Intercept the confirmation API call
|
||||
cy.intercept('POST', '**/user/confirm').as('confirmEmail')
|
||||
|
||||
// Try to confirm with an invalid token
|
||||
const invalidToken = 'invalid-token-that-does-not-exist-in-database'
|
||||
cy.visit('/login', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('emailConfirmToken', invalidToken)
|
||||
},
|
||||
})
|
||||
|
||||
// Wait for the confirmation API call to fail
|
||||
cy.wait('@confirmEmail', {timeout: 10000})
|
||||
|
||||
// Should show error message
|
||||
cy.get('.message.danger', {timeout: 10000}).should('be.visible')
|
||||
|
||||
// Login should still fail
|
||||
cy.get('input[id=username]').type(user.username)
|
||||
cy.get('input[id=password]').type('1234')
|
||||
cy.get('.button').contains('Login').click()
|
||||
|
||||
cy.get('div.message.danger').contains('Email address of the user not confirmed')
|
||||
})
|
||||
|
||||
it('Should not allow using the same token twice', () => {
|
||||
// Intercept the confirmation API call
|
||||
cy.intercept('POST', '**/user/confirm').as('confirmEmail')
|
||||
|
||||
// First confirmation - should work
|
||||
cy.visit('/login', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('emailConfirmToken', confirmationToken)
|
||||
},
|
||||
})
|
||||
cy.wait('@confirmEmail', {timeout: 10000}).its('response.statusCode').should('eq', 200)
|
||||
cy.get('.message.success', {timeout: 10000}).should('be.visible')
|
||||
cy.get('.message.success').contains('You successfully confirmed your email')
|
||||
|
||||
// Try to use the same token again - should fail
|
||||
cy.visit('/login', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('emailConfirmToken', confirmationToken)
|
||||
},
|
||||
})
|
||||
cy.wait('@confirmEmail', {timeout: 10000})
|
||||
cy.get('.message.danger', {timeout: 10000}).should('be.visible')
|
||||
})
|
||||
|
||||
it('Should confirm email when clicking link from email (via query parameter)', () => {
|
||||
// Intercept the confirmation API call
|
||||
cy.intercept('POST', '**/user/confirm').as('confirmEmail')
|
||||
|
||||
// Simulate clicking the email confirmation link with query parameter
|
||||
// This is what happens when a user clicks the link in their email
|
||||
cy.visit(`/?userEmailConfirm=${confirmationToken}`)
|
||||
|
||||
// Should redirect to login page
|
||||
cy.url().should('include', '/login')
|
||||
|
||||
// Wait for the confirmation API call to complete
|
||||
cy.wait('@confirmEmail', {timeout: 10000}).its('response.statusCode').should('eq', 200)
|
||||
|
||||
// Should show success message
|
||||
cy.get('.message.success', {timeout: 10000}).should('be.visible')
|
||||
cy.get('.message.success').contains('You successfully confirmed your email')
|
||||
|
||||
// Now login should work
|
||||
cy.get('input[id=username]').type(user.username)
|
||||
cy.get('input[id=password]').type('1234')
|
||||
cy.get('.button').contains('Login').click()
|
||||
|
||||
// Should successfully log in
|
||||
cy.url().should('include', '/')
|
||||
cy.url().should('not.include', '/login')
|
||||
// Check that the username appears in the greeting
|
||||
cy.contains(user.username)
|
||||
})
|
||||
})
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import {computed, watch} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
|
||||
|
|
@ -55,7 +55,6 @@ import {success} from '@/message'
|
|||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
useBodyClass('is-touch', isTouchDevice())
|
||||
|
|
@ -77,17 +76,6 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
|||
authStore.refreshUserInfo()
|
||||
}, { immediate: true })
|
||||
|
||||
// setup email verification redirect
|
||||
const userEmailConfirm = computed(() => route.query?.userEmailConfirm as (string | undefined))
|
||||
watch(userEmailConfirm, (userEmailConfirm) => {
|
||||
if (userEmailConfirm === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('emailConfirmToken', userEmailConfirm)
|
||||
router.push({name: 'user.login'})
|
||||
}, { immediate: true })
|
||||
|
||||
setLanguage(authStore.settings.language)
|
||||
useColorScheme()
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -408,7 +408,18 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
|||
return {name: 'user.login'}
|
||||
}
|
||||
|
||||
// Check if the route the user wants to go to is a route which needs authentication. We use this to
|
||||
// Check if email confirmation token is in query params
|
||||
const emailConfirmToken = to.query.userEmailConfirm as string | undefined
|
||||
if (emailConfirmToken) {
|
||||
// Save token to localStorage before redirecting
|
||||
localStorage.setItem('emailConfirmToken', emailConfirmToken)
|
||||
// Redirect to login page where it will be processed
|
||||
if (to.name !== 'user.login') {
|
||||
return {name: 'user.login'}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the route the user wants to go to is a route which needs authentication. We use this to
|
||||
// redirect the user after successful login.
|
||||
const isValidUserAppRoute = ![
|
||||
'user.login',
|
||||
|
|
@ -429,7 +440,7 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
|||
}
|
||||
|
||||
if(localStorage.getItem('emailConfirmToken') !== null && to.name !== 'user.login') {
|
||||
return {name: 'user.login'}
|
||||
return {name: 'user.login', query: to.query}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue