From 968db75d7a500ff5d6bd2ae759afcc2918bec3dd Mon Sep 17 00:00:00 2001 From: Lera24 Date: Wed, 15 May 2024 16:44:31 +0300 Subject: [PATCH 1/3] feature: add 2fa[WTEL-4430] --- src/api/auth/auth.js | 16 ++++- .../auth/login/steps/the-login-third-step.vue | 58 +++++++++++++++ src/components/auth/login/the-login.vue | 70 ++++++++++++++----- src/locale/en/en.js | 2 + src/locale/ru/ru.js | 2 + src/locale/ua/ua.js | 2 + src/store/modules/auth/auth.js | 44 +++++++++--- 7 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 src/components/auth/login/steps/the-login-third-step.vue diff --git a/src/api/auth/auth.js b/src/api/auth/auth.js index 748d12f6..af1d4efd 100644 --- a/src/api/auth/auth.js +++ b/src/api/auth/auth.js @@ -1,8 +1,21 @@ import instance, { config } from '../instance'; -export const login = async (credentials) => { +export const login = async (credentials, isToken = true) => { const url = '/login'; + try { + const response = await instance.post(url, credentials); + if(isToken) { + localStorage.setItem('access-token', response.accessToken); + return postToken(); + } return response; + } catch (err) { + throw err; + } +}; + +export const login2fa = async (credentials) => { + const url = '/login/2fa'; try { const response = await instance.post(url, credentials); localStorage.setItem('access-token', response.accessToken); @@ -105,6 +118,7 @@ const checkDomainExistence = async (domain) => { const AuthAPI = { login, + login2fa, register, checkCurrentSession, loadServiceProviders, diff --git a/src/components/auth/login/steps/the-login-third-step.vue b/src/components/auth/login/steps/the-login-third-step.vue new file mode 100644 index 00000000..ff8eeea1 --- /dev/null +++ b/src/components/auth/login/steps/the-login-third-step.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/auth/login/the-login.vue b/src/components/auth/login/the-login.vue index f5fb7917..2671432e 100644 --- a/src/components/auth/login/the-login.vue +++ b/src/components/auth/login/the-login.vue @@ -21,6 +21,12 @@ @back="backPrevStep" @next="goNextStep" > + + @@ -28,24 +34,39 @@ diff --git a/src/locale/en/en.js b/src/locale/en/en.js index 06907cc1..f192ca3f 100644 --- a/src/locale/en/en.js +++ b/src/locale/en/en.js @@ -16,6 +16,8 @@ export default { signIn: 'Sign in', login: 'Log in', remember: 'Remember', + code: 'Code', + enterAuthenticationCode: 'Enter your two-factor authentication code', carousel: { title1: 'Cloud vs. On-Site', text1: 'Security policy does not allow to store data and use cloud services? With Webitel, you can build a contact center on your site!', diff --git a/src/locale/ru/ru.js b/src/locale/ru/ru.js index 22d748aa..842b648f 100644 --- a/src/locale/ru/ru.js +++ b/src/locale/ru/ru.js @@ -16,5 +16,7 @@ export default { signIn: 'Войти', login: 'Войти', remember: 'Запомнить', + code: 'Код', + enterAuthenticationCode: 'Введите ваш код двухфакторной аутентификации', }, }; diff --git a/src/locale/ua/ua.js b/src/locale/ua/ua.js index 31fb2c3d..c309e70a 100644 --- a/src/locale/ua/ua.js +++ b/src/locale/ua/ua.js @@ -16,5 +16,7 @@ export default { signIn: 'Увійти', login: 'Увійти', remember: 'Запам\'ятати', + code: 'Код', + enterAuthenticationCode: 'Введіть ваш код двофакторної автентифікації', }, }; diff --git a/src/store/modules/auth/auth.js b/src/store/modules/auth/auth.js index d2113a0e..34c3d35e 100644 --- a/src/store/modules/auth/auth.js +++ b/src/store/modules/auth/auth.js @@ -14,6 +14,9 @@ const state = { ...defaultState(), rememberCredentials: localStorage.getItem('auth/rememberCredentials') === 'true', loginProviders: {}, + enabledTfa: '', + totp: '', + id: '', }; const getters = {}; @@ -21,16 +24,15 @@ const getters = {}; const actions = { SUBMIT_AUTH: async (context, action) => { let accessToken; - switch (action) { - case 'login': - accessToken = await context.dispatch('LOGIN'); - break; - case 'register': - accessToken = await context.dispatch('REGISTER'); - break; - default: - throw new Error(`Invalid action: ${action}`); - } + + if(action === 'login') { + if(context.state.id) { + accessToken = await context.dispatch('LOGIN_2FA'); + } + accessToken = await context.dispatch('LOGIN'); + } else if(action === 'register') { + accessToken = await context.dispatch('REGISTER'); + } else throw new Error(`Invalid action: ${action}`); return context.dispatch('ON_AUTH_SUCCESS', { accessToken }); }, @@ -43,6 +45,25 @@ const actions = { }); }, + GET_2FA_SESSION_ID: async (context) => { + const { id } = await AuthAPI.login({ + username: context.state.username, + password: context.state.password, + domain: context.state.domain, + }, false); + + if(id) { + await context.dispatch('SET_PROPERTY', { prop: 'id', value: id }); + } + }, + + LOGIN_2FA: (context) => { + return AuthAPI.login2fa({ + id: context.state.id, + totp: context.state.totp, + }) + }, + REGISTER: (context) => { return AuthAPI.register({ username: context.state.username, @@ -55,8 +76,9 @@ const actions = { LOAD_SERVICE_PROVIDERS: async (context) => { const domain = context.state.domain; const response = await AuthAPI.loadServiceProviders({ domain }); - const { federation = {} } = response; + const { federation = {}, enabledTfa } = response; context.commit('SET_SERVICE_PROVIDERS', federation); + await context.dispatch('SET_PROPERTY', { prop: 'enabledTfa', value: enabledTfa }); }, EXECUTE_PROVIDER: (context, { ticket }) => { From f04882a9ecbf31632dfcdf0819fd10d91f7e0b48 Mon Sep 17 00:00:00 2001 From: Lera24 Date: Wed, 15 May 2024 22:02:53 +0300 Subject: [PATCH 2/3] fix: rename param id, change steps computed and change store and API for auth[WTEL-3405] --- src/api/auth/auth.js | 4 +- src/components/auth/login/the-login.vue | 51 +++++++++++-------------- src/store/modules/auth/auth.js | 26 +++++-------- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/src/api/auth/auth.js b/src/api/auth/auth.js index af1d4efd..05308a2b 100644 --- a/src/api/auth/auth.js +++ b/src/api/auth/auth.js @@ -1,11 +1,11 @@ import instance, { config } from '../instance'; -export const login = async (credentials, isToken = true) => { +export const login = async (credentials) => { const url = '/login'; try { const response = await instance.post(url, credentials); - if(isToken) { + if(response.accessToken) { localStorage.setItem('access-token', response.accessToken); return postToken(); } return response; diff --git a/src/components/auth/login/the-login.vue b/src/components/auth/login/the-login.vue index 2671432e..12a07a25 100644 --- a/src/components/auth/login/the-login.vue +++ b/src/components/auth/login/the-login.vue @@ -43,7 +43,6 @@ export default { data: () => ({ activeStep: 1, isFirstStepSubmitting: false, - steps: [], }), components: { FirstStep, @@ -55,18 +54,9 @@ export default { ...mapState('auth', { enabledTfa: (state) => state.enabledTfa, }), - }, - methods: { - ...mapActions('auth', { - setProp: 'SET_PROPERTY', - resetState: 'RESET_STATE', - checkDomain: 'CHECK_DOMAIN', - get2faSessionId: 'GET_2FA_SESSION_ID', - }), - - setSteps() { - this.steps = [ + steps() { + const arr = [ { name: this.$t('reusable.step', { count: 1 }), description: this.$t('auth.enterDomain'), @@ -75,8 +65,28 @@ export default { name: this.$t('reusable.step', { count: 2 }), description: this.$t('auth.enterUsername'), }, - ] + ]; + + if (this.enabledTfa) arr.push({ + name: this.$t('reusable.step', { count: 3 }), + description: this.$t('auth.enterAuthenticationCode'), + }); + + return arr; }, + }, + + methods: { + ...mapActions('auth', { + setProp: 'SET_PROPERTY', + resetState: 'RESET_STATE', + checkDomain: 'CHECK_DOMAIN', + + // [https://webitel.atlassian.net/browse/WTEL-3405] + // post request /login returns different values for two-factor authentication and standard login + + get2faSessionId: 'LOGIN', + }), backPrevStep() { if (this.activeStep === 1) { @@ -118,25 +128,10 @@ export default { }, }, - created() { - this.setSteps(); - }, - unmounted() { this.resetState(); }, - watch: { - enabledTfa: { - handler(value) { - if (value) this.steps.push({ - name: this.$t('reusable.step', { count: 3 }), - description: this.$t('auth.enterAuthenticationCode'), - }) - }, - }, - }, - }; diff --git a/src/store/modules/auth/auth.js b/src/store/modules/auth/auth.js index 34c3d35e..a5d6233b 100644 --- a/src/store/modules/auth/auth.js +++ b/src/store/modules/auth/auth.js @@ -14,9 +14,9 @@ const state = { ...defaultState(), rememberCredentials: localStorage.getItem('auth/rememberCredentials') === 'true', loginProviders: {}, - enabledTfa: '', + enabledTfa: false, totp: '', - id: '', + sessionId: '', // it's necessary for two-factor authentication }; const getters = {}; @@ -26,7 +26,7 @@ const actions = { let accessToken; if(action === 'login') { - if(context.state.id) { + if(context.state.sessionId) { accessToken = await context.dispatch('LOGIN_2FA'); } accessToken = await context.dispatch('LOGIN'); @@ -37,29 +37,23 @@ const actions = { return context.dispatch('ON_AUTH_SUCCESS', { accessToken }); }, - LOGIN: (context) => { - return AuthAPI.login({ + LOGIN: async (context) => { + const response = await AuthAPI.login({ username: context.state.username, password: context.state.password, domain: context.state.domain, }); - }, - - GET_2FA_SESSION_ID: async (context) => { - const { id } = await AuthAPI.login({ - username: context.state.username, - password: context.state.password, - domain: context.state.domain, - }, false); - if(id) { - await context.dispatch('SET_PROPERTY', { prop: 'id', value: id }); + if(response?.id) { + await context.dispatch('SET_PROPERTY', { prop: 'sessionId', value: response.id }); } + + return response; }, LOGIN_2FA: (context) => { return AuthAPI.login2fa({ - id: context.state.id, + id: context.state.sessionId, totp: context.state.totp, }) }, From 4e1235b6e07d1591dbf678cc0ca6bed9ab491b3e Mon Sep 17 00:00:00 2001 From: Lera24 Date: Thu, 16 May 2024 13:11:38 +0300 Subject: [PATCH 3/3] fix: add method get2SessionId in store, add comment for API and rename const in steps computed[WTEL-3405] --- src/api/auth/auth.js | 6 ++++++ src/components/auth/login/the-login.vue | 12 ++++-------- src/store/modules/auth/auth.js | 24 +++++++++++++++--------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/api/auth/auth.js b/src/api/auth/auth.js index 05308a2b..7597ed93 100644 --- a/src/api/auth/auth.js +++ b/src/api/auth/auth.js @@ -5,6 +5,12 @@ export const login = async (credentials) => { try { const response = await instance.post(url, credentials); + + // [https://webitel.atlassian.net/browse/WTEL-3405] + // If two-factor authentication is enabled, + // API returns the two-factor authentication session ID instead of a token + // and saving to localStorage is not needed + if(response.accessToken) { localStorage.setItem('access-token', response.accessToken); return postToken(); diff --git a/src/components/auth/login/the-login.vue b/src/components/auth/login/the-login.vue index 12a07a25..af307def 100644 --- a/src/components/auth/login/the-login.vue +++ b/src/components/auth/login/the-login.vue @@ -56,7 +56,7 @@ export default { }), steps() { - const arr = [ + const steps = [ { name: this.$t('reusable.step', { count: 1 }), description: this.$t('auth.enterDomain'), @@ -67,12 +67,12 @@ export default { }, ]; - if (this.enabledTfa) arr.push({ + if (this.enabledTfa) steps.push({ name: this.$t('reusable.step', { count: 3 }), description: this.$t('auth.enterAuthenticationCode'), }); - return arr; + return steps; }, }, @@ -81,11 +81,7 @@ export default { setProp: 'SET_PROPERTY', resetState: 'RESET_STATE', checkDomain: 'CHECK_DOMAIN', - - // [https://webitel.atlassian.net/browse/WTEL-3405] - // post request /login returns different values for two-factor authentication and standard login - - get2faSessionId: 'LOGIN', + get2faSessionId: 'GET_2FA_SESSION_ID', }), backPrevStep() { diff --git a/src/store/modules/auth/auth.js b/src/store/modules/auth/auth.js index a5d6233b..59573094 100644 --- a/src/store/modules/auth/auth.js +++ b/src/store/modules/auth/auth.js @@ -38,17 +38,20 @@ const actions = { }, LOGIN: async (context) => { - const response = await AuthAPI.login({ + return await AuthAPI.login({ username: context.state.username, password: context.state.password, domain: context.state.domain, }); + }, - if(response?.id) { - await context.dispatch('SET_PROPERTY', { prop: 'sessionId', value: response.id }); - } - - return response; + REGISTER: (context) => { + return AuthAPI.register({ + username: context.state.username, + password: context.state.password, + certificate: context.state.certificate, + domain: context.state.domain, + }); }, LOGIN_2FA: (context) => { @@ -58,13 +61,16 @@ const actions = { }) }, - REGISTER: (context) => { - return AuthAPI.register({ + GET_2FA_SESSION_ID: async (context) => { + const { id } = await AuthAPI.login({ username: context.state.username, password: context.state.password, - certificate: context.state.certificate, domain: context.state.domain, }); + + if(id) { + await context.dispatch('SET_PROPERTY', { prop: 'sessionId', value: id }); + } }, LOAD_SERVICE_PROVIDERS: async (context) => {