From 77eceedbf7a5b53948d7d91b3d228aa303c02081 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 12 Jun 2019 20:16:55 +0000 Subject: Revert "add TOTP/Recovery Form for mobile version" This reverts commit a3811f944819430c278b6da6b08dc322a9b9ff65. --- src/components/auth_form/auth_form.js | 26 ++++ src/components/login_form/login_form.js | 63 +++++---- src/components/login_form/login_form.vue | 68 +++++----- src/components/mfa_form/recovery_form.js | 41 ++++++ src/components/mfa_form/recovery_form.vue | 42 ++++++ src/components/mfa_form/totp_form.js | 40 ++++++ src/components/mfa_form/totp_form.vue | 45 +++++++ src/components/user_panel/user_panel.js | 8 +- src/components/user_panel/user_panel.vue | 11 +- src/components/user_settings/confirm.js | 9 ++ src/components/user_settings/confirm.vue | 14 ++ src/components/user_settings/mfa.js | 152 ++++++++++++++++++++++ src/components/user_settings/mfa.vue | 121 +++++++++++++++++ src/components/user_settings/mfa_backup_codes.js | 17 +++ src/components/user_settings/mfa_backup_codes.vue | 22 ++++ src/components/user_settings/mfa_totp.js | 49 +++++++ src/components/user_settings/mfa_totp.vue | 23 ++++ src/components/user_settings/user_settings.js | 4 +- src/components/user_settings/user_settings.vue | 2 +- 19 files changed, 697 insertions(+), 60 deletions(-) create mode 100644 src/components/auth_form/auth_form.js create mode 100644 src/components/mfa_form/recovery_form.js create mode 100644 src/components/mfa_form/recovery_form.vue create mode 100644 src/components/mfa_form/totp_form.js create mode 100644 src/components/mfa_form/totp_form.vue create mode 100644 src/components/user_settings/confirm.js create mode 100644 src/components/user_settings/confirm.vue create mode 100644 src/components/user_settings/mfa.js create mode 100644 src/components/user_settings/mfa.vue create mode 100644 src/components/user_settings/mfa_backup_codes.js create mode 100644 src/components/user_settings/mfa_backup_codes.vue create mode 100644 src/components/user_settings/mfa_totp.js create mode 100644 src/components/user_settings/mfa_totp.vue (limited to 'src/components') diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js new file mode 100644 index 00000000..e9a6e2d5 --- /dev/null +++ b/src/components/auth_form/auth_form.js @@ -0,0 +1,26 @@ +import LoginForm from '../login_form/login_form.vue' +import MFARecoveryForm from '../mfa_form/recovery_form.vue' +import MFATOTPForm from '../mfa_form/totp_form.vue' +import { mapGetters } from 'vuex' + +const AuthForm = { + name: 'AuthForm', + render (createElement) { + return createElement('component', { is: this.authForm }) + }, + computed: { + authForm () { + if (this.requiredTOTP) { return 'MFATOTPForm' } + if (this.requiredRecovery) { return 'MFARecoveryForm' } + return 'LoginForm' + }, + ...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery']) + }, + components: { + MFARecoveryForm, + MFATOTPForm, + LoginForm + } +} + +export default AuthForm diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index dc917e47..1119754e 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,28 +1,44 @@ +import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import oauthApi from '../../services/new_api/oauth.js' + const LoginForm = { data: () => ({ user: {}, - authError: false + error: false }), computed: { - loginMethod () { return this.$store.state.instance.loginMethod }, - loggingIn () { return this.$store.state.users.loggingIn }, - registrationOpen () { return this.$store.state.instance.registrationOpen } + isPasswordAuth () { return this.requiredPassword }, + isTokenAuth () { return this.requiredToken }, + ...mapState({ + registrationOpen: state => state.instance.registrationOpen, + instance: state => state.instance, + loggingIn: state => state.users.loggingIn, + oauth: state => state.oauth + }), + ...mapGetters( + 'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA'] + ) }, methods: { - oAuthLogin () { + ...mapMutations('authFlow', ['requireMFA']), + ...mapActions({ login: 'authFlow/login' }), + submit () { + this.isTokenMethod ? this.submitToken() : this.submitPassword() + }, + submitToken () { oauthApi.login({ - oauth: this.$store.state.oauth, - instance: this.$store.state.instance.server, + oauth: this.oauth, + instance: this.instance.server, commit: this.$store.commit }) }, - submit () { + submitPassword () { const data = { - oauth: this.$store.state.oauth, - instance: this.$store.state.instance.server + oauth: this.oauth, + instance: this.instance.server } - this.clearError() + this.error = false + oauthApi.getOrCreateApp(data).then((app) => { oauthApi.getTokenWithCredentials( { @@ -31,24 +47,27 @@ const LoginForm = { username: this.user.username, password: this.user.password } - ).then(async (result) => { + ).then((result) => { if (result.error) { - this.authError = result.error - this.user.password = '' + if (result.error === 'mfa_required') { + this.requireMFA({app: app, settings: result}) + } else { + this.error = result.error + this.focusOnPasswordInput() + } return } - this.$store.commit('setToken', result.access_token) - try { - await this.$store.dispatch('loginUser', result.access_token) + this.login(result).then(() => { this.$router.push({name: 'friends'}) - } catch (e) { - console.log(e) - } + }) }) }) }, - clearError () { - this.authError = false + clearError () { this.error = false }, + focusOnPasswordInput () { + let passwordInput = this.$refs.passwordInput + passwordInput.focus() + passwordInput.setSelectionRange(0, passwordInput.value.length) } } } diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index c6be2e00..a2c5cf8f 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -1,47 +1,53 @@ diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js new file mode 100644 index 00000000..fbe9b437 --- /dev/null +++ b/src/components/mfa_form/recovery_form.js @@ -0,0 +1,41 @@ +import mfaApi from '../../services/new_api/mfa.js' +import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' + +export default { + data: () => ({ + code: null, + error: false + }), + computed: { + ...mapGetters({ + authApp: 'authFlow/app', + authSettings: 'authFlow/settings' + }), + ...mapState({ instance: 'instance' }) + }, + methods: { + ...mapMutations('authFlow', ['requireTOTP', 'abortMFA']), + ...mapActions({ login: 'authFlow/login' }), + clearError () { this.error = false }, + submit () { + const data = { + app: this.authApp, + instance: this.instance.server, + mfaToken: this.authSettings.mfa_token, + code: this.code + } + + mfaApi.verifyRecoveryCode(data).then((result) => { + if (result.error) { + this.error = result.error + this.code = null + return + } + + this.login(result).then(() => { + this.$router.push({name: 'friends'}) + }) + }) + } + } +} diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue new file mode 100644 index 00000000..e0e2d65b --- /dev/null +++ b/src/components/mfa_form/recovery_form.vue @@ -0,0 +1,42 @@ + + diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js new file mode 100644 index 00000000..6c94fe52 --- /dev/null +++ b/src/components/mfa_form/totp_form.js @@ -0,0 +1,40 @@ +import mfaApi from '../../services/new_api/mfa.js' +import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' +export default { + data: () => ({ + code: null, + error: false + }), + computed: { + ...mapGetters({ + authApp: 'authFlow/app', + authSettings: 'authFlow/settings' + }), + ...mapState({ instance: 'instance' }) + }, + methods: { + ...mapMutations('authFlow', ['requireRecovery', 'abortMFA']), + ...mapActions({ login: 'authFlow/login' }), + clearError () { this.error = false }, + submit () { + const data = { + app: this.authApp, + instance: this.instance.server, + mfaToken: this.authSettings.mfa_token, + code: this.code + } + + mfaApi.verifyOTPCode(data).then((result) => { + if (result.error) { + this.error = result.error + this.code = null + return + } + + this.login(result).then(() => { + this.$router.push({name: 'friends'}) + }) + }) + } + } +} diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue new file mode 100644 index 00000000..c547785e --- /dev/null +++ b/src/components/mfa_form/totp_form.vue @@ -0,0 +1,45 @@ + + diff --git a/src/components/user_panel/user_panel.js b/src/components/user_panel/user_panel.js index d4478290..c2f51eb6 100644 --- a/src/components/user_panel/user_panel.js +++ b/src/components/user_panel/user_panel.js @@ -1,13 +1,15 @@ -import LoginForm from '../login_form/login_form.vue' +import AuthForm from '../auth_form/auth_form.js' import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCard from '../user_card/user_card.vue' +import { mapState } from 'vuex' const UserPanel = { computed: { - user () { return this.$store.state.users.currentUser } + signedIn () { return this.user }, + ...mapState({ user: state => state.users.currentUser }) }, components: { - LoginForm, + AuthForm, PostStatusForm, UserCard } diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue index 8310f30e..37e28ca5 100644 --- a/src/components/user_panel/user_panel.vue +++ b/src/components/user_panel/user_panel.vue @@ -1,13 +1,20 @@ + + diff --git a/src/components/user_settings/confirm.js b/src/components/user_settings/confirm.js new file mode 100644 index 00000000..0f4ddfc9 --- /dev/null +++ b/src/components/user_settings/confirm.js @@ -0,0 +1,9 @@ +const Confirm = { + props: ['disabled'], + data: () => ({}), + methods: { + confirm () { this.$emit('confirm') }, + cancel () { this.$emit('cancel') } + } +} +export default Confirm diff --git a/src/components/user_settings/confirm.vue b/src/components/user_settings/confirm.vue new file mode 100644 index 00000000..46a42e38 --- /dev/null +++ b/src/components/user_settings/confirm.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js new file mode 100644 index 00000000..2acee862 --- /dev/null +++ b/src/components/user_settings/mfa.js @@ -0,0 +1,152 @@ +import RecoveryCodes from './mfa_backup_codes.vue' +import TOTP from './mfa_totp.vue' +import Confirm from './confirm.vue' +import VueQrcode from '@chenfengyuan/vue-qrcode' +import { mapState } from 'vuex' + +const Mfa = { + data: () => ({ + settings: { // current settings of MFA + enabled: false, + totp: false + }, + setupState: { // setup mfa + state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete' + setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete' + }, + backupCodes: { + getNewCodes: false, + inProgress: false, // progress of fetch codes + codes: [] + }, + otpSettings: { // pre-setup setting of OTP. secret key, qrcode url. + provisioning_uri: '', + key: '' + }, + currentPassword: null, + otpConfirmToken: null, + error: null, + readyInit: false + }), + components: { + 'recovery-codes': RecoveryCodes, + 'totp-item': TOTP, + 'qrcode': VueQrcode, + 'confirm': Confirm + }, + computed: { + canSetupOTP () { + return ( + (this.setupInProgress && this.backupCodesPrepared) || + this.settings.enabled + ) && !this.settings.totp && !this.setupOTPInProgress + }, + setupInProgress () { + return this.setupState.state !== '' && this.setupState.state !== 'complete' + }, + setupOTPInProgress () { + return this.setupState.state === 'setupOTP' && !this.completedOTP + }, + prepareOTP () { + return this.setupState.setupOTPState === 'prepare' + }, + confirmOTP () { + return this.setupState.setupOTPState === 'confirm' + }, + completedOTP () { + return this.setupState.setupOTPState === 'completed' + }, + backupCodesPrepared () { + return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0 + }, + confirmNewBackupCodes () { + return this.backupCodes.getNewCodes + }, + ...mapState({ + backendInteractor: (state) => state.api.backendInteractor + }) + }, + + methods: { + activateOTP () { + if (!this.settings.enabled) { + this.setupState.state = 'getBackupcodes' + this.fetchBackupCodes() + } + }, + fetchBackupCodes () { + this.backupCodes.inProgress = true + this.backupCodes.codes = [] + + return this.backendInteractor.generateMfaBackupCodes() + .then((res) => { + this.backupCodes.codes = res.codes + this.backupCodes.inProgress = false + }) + }, + getBackupCodes () { // get a new backup codes + this.backupCodes.getNewCodes = true + }, + confirmBackupCodes () { // confirm getting new backup codes + this.fetchBackupCodes().then((res) => { + this.backupCodes.getNewCodes = false + }) + }, + cancelBackupCodes () { // cancel confirm form of new backup codes + this.backupCodes.getNewCodes = false + }, + + // Setup OTP + setupOTP () { // prepare setup OTP + this.setupState.state = 'setupOTP' + this.setupState.setupOTPState = 'prepare' + this.backendInteractor.mfaSetupOTP() + .then((res) => { + this.otpSettings = res + this.setupState.setupOTPState = 'confirm' + }) + }, + doConfirmOTP () { // handler confirm enable OTP + this.error = null + this.backendInteractor.mfaConfirmOTP({ + token: this.otpConfirmToken, + password: this.currentPassword + }) + .then((res) => { + if (res.error) { + this.error = res.error + return + } + this.completeSetup() + }) + }, + + completeSetup () { + this.setupState.setupOTPState = 'complete' + this.setupState.state = 'complete' + this.currentPassword = null + this.error = null + this.fetchSettings() + }, + cancelSetup () { // cancel setup + this.setupState.setupOTPState = '' + this.setupState.state = '' + this.currentPassword = null + this.error = null + }, + // end Setup OTP + + // fetch settings from server + async fetchSettings () { + let result = await this.backendInteractor.fetchSettingsMFA() + this.settings = result.settings + return result + } + }, + mounted () { + this.fetchSettings().then(() => { + this.readyInit = true + }) + } +} +export default Mfa diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue new file mode 100644 index 00000000..ded426dd --- /dev/null +++ b/src/components/user_settings/mfa.vue @@ -0,0 +1,121 @@ + + + + diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/user_settings/mfa_backup_codes.js new file mode 100644 index 00000000..f0a984ec --- /dev/null +++ b/src/components/user_settings/mfa_backup_codes.js @@ -0,0 +1,17 @@ +export default { + props: { + backupCodes: { + type: Object, + default: () => ({ + inProgress: false, + codes: [] + }) + } + }, + data: () => ({}), + computed: { + inProgress () { return this.backupCodes.inProgress }, + ready () { return this.backupCodes.codes.length > 0 }, + displayTitle () { return this.inProgress || this.ready } + } +} diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue new file mode 100644 index 00000000..c275bd63 --- /dev/null +++ b/src/components/user_settings/mfa_backup_codes.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/user_settings/mfa_totp.js b/src/components/user_settings/mfa_totp.js new file mode 100644 index 00000000..8408d8e9 --- /dev/null +++ b/src/components/user_settings/mfa_totp.js @@ -0,0 +1,49 @@ +import Confirm from './confirm.vue' +import { mapState } from 'vuex' + +export default { + props: ['settings'], + data: () => ({ + error: false, + currentPassword: '', + deactivate: false, + inProgress: false // progress peform request to disable otp method + }), + components: { + 'confirm': Confirm + }, + computed: { + isActivated () { + return this.settings.totp + }, + ...mapState({ + backendInteractor: (state) => state.api.backendInteractor + }) + }, + methods: { + doActivate () { + this.$emit('activate') + }, + cancelDeactivate () { this.deactivate = false }, + doDeactivate () { + this.error = null + this.deactivate = true + }, + confirmDeactivate () { // confirm deactivate TOTP method + this.error = null + this.inProgress = true + this.backendInteractor.mfaDisableOTP({ + password: this.currentPassword + }) + .then((res) => { + this.inProgress = false + if (res.error) { + this.error = res.error + return + } + this.deactivate = false + this.$emit('deactivate') + }) + } + } +} diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/user_settings/mfa_totp.vue new file mode 100644 index 00000000..6b73c8f4 --- /dev/null +++ b/src/components/user_settings/mfa_totp.vue @@ -0,0 +1,23 @@ + + diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index ae36e5e8..69505806 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -17,6 +17,7 @@ import Importer from '../importer/importer.vue' import Exporter from '../exporter/exporter.vue' import withSubscription from '../../hocs/with_subscription/with_subscription' import userSearchApi from '../../services/new_api/user_search.js' +import Mfa from './mfa.vue' const BlockList = withSubscription({ fetch: (props, $store) => $store.dispatch('fetchBlocks'), @@ -75,7 +76,8 @@ const UserSettings = { MuteCard, ProgressButton, Importer, - Exporter + Exporter, + Mfa }, computed: { user () { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 15a5428c..bbe41f11 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -152,7 +152,7 @@ - +

{{$t('settings.delete_account')}}

{{$t('settings.delete_account_description')}}

-- cgit v1.2.3-70-g09d2