diff options
Diffstat (limited to 'src/components/settings_modal/tabs/security_tab')
10 files changed, 752 insertions, 0 deletions
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.js b/src/components/settings_modal/tabs/security_tab/confirm.js new file mode 100644 index 00000000..0f4ddfc9 --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/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/settings_modal/tabs/security_tab/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue new file mode 100644 index 00000000..69b3811b --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/confirm.vue @@ -0,0 +1,22 @@ +<template> + <div> + <slot /> + <button + class="btn btn-default" + :disabled="disabled" + @click="confirm" + > + {{ $t('general.confirm') }} + </button> + <button + class="btn btn-default" + :disabled="disabled" + @click="cancel" + > + {{ $t('general.cancel') }} + </button> + </div> +</template> + +<script src="./confirm.js"> +</script> diff --git a/src/components/settings_modal/tabs/security_tab/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js new file mode 100644 index 00000000..abf37062 --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/mfa.js @@ -0,0 +1,155 @@ +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 + available: false, + 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.settingsMFA() + if (result.error) return + this.settings = result.settings + this.settings.available = true + return result + } + }, + mounted () { + this.fetchSettings().then(() => { + this.readyInit = true + }) + } +} +export default Mfa diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue new file mode 100644 index 00000000..7aca3c8d --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -0,0 +1,173 @@ +<template> + <div + v-if="readyInit && settings.available" + class="setting-item mfa-settings" + > + <div class="mfa-heading"> + <h2>{{ $t('settings.mfa.title') }}</h2> + </div> + + <div> + <div + v-if="!setupInProgress" + class="setting-item" + > + <!-- Enabled methods --> + <h3>{{ $t('settings.mfa.authentication_methods') }}</h3> + <totp-item + :settings="settings" + @deactivate="fetchSettings" + @activate="activateOTP" + /> + <br> + + <div v-if="settings.enabled"> + <!-- backup codes block--> + <recovery-codes + v-if="!confirmNewBackupCodes" + :backup-codes="backupCodes" + /> + <button + v-if="!confirmNewBackupCodes" + class="btn btn-default" + @click="getBackupCodes" + > + {{ $t('settings.mfa.generate_new_recovery_codes') }} + </button> + + <div v-if="confirmNewBackupCodes"> + <confirm + :disabled="backupCodes.inProgress" + @confirm="confirmBackupCodes" + @cancel="cancelBackupCodes" + > + <p class="warning"> + {{ $t('settings.mfa.warning_of_generate_new_codes') }} + </p> + </confirm> + </div> + </div> + </div> + + <div v-if="setupInProgress"> + <!-- setup block--> + + <h3>{{ $t('settings.mfa.setup_otp') }}</h3> + + <recovery-codes + v-if="!setupOTPInProgress" + :backup-codes="backupCodes" + /> + + <button + v-if="canSetupOTP" + class="btn btn-default" + @click="cancelSetup" + > + {{ $t('general.cancel') }} + </button> + + <button + v-if="canSetupOTP" + class="btn btn-default" + @click="setupOTP" + > + {{ $t('settings.mfa.setup_otp') }} + </button> + + <template v-if="setupOTPInProgress"> + <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i> + + <div v-if="confirmOTP"> + <div class="setup-otp"> + <div class="qr-code"> + <h4>{{ $t('settings.mfa.scan.title') }}</h4> + <p>{{ $t('settings.mfa.scan.desc') }}</p> + <qrcode + :value="otpSettings.provisioning_uri" + :options="{ width: 200 }" + /> + <p> + {{ $t('settings.mfa.scan.secret_code') }}: + {{ otpSettings.key }} + </p> + </div> + + <div class="verify"> + <h4>{{ $t('general.verify') }}</h4> + <p>{{ $t('settings.mfa.verify.desc') }}</p> + <input + v-model="otpConfirmToken" + type="text" + > + + <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p> + <input + v-model="currentPassword" + type="password" + > + <div class="confirm-otp-actions"> + <button + class="btn btn-default" + @click="doConfirmOTP" + > + {{ $t('settings.mfa.confirm_and_enable') }} + </button> + <button + class="btn btn-default" + @click="cancelSetup" + > + {{ $t('general.cancel') }} + </button> + </div> + <div + v-if="error" + class="alert error" + > + {{ error }} + </div> + </div> + </div> + </div> + </template> + </div> + </div> + </div> +</template> + +<script src="./mfa.js"></script> +<style lang="scss"> +@import '../../../../_variables.scss'; +.mfa-settings { + .mfa-heading, .method-item { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + } + + .warning { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + + .setup-otp { + display: flex; + justify-content: center; + flex-wrap: wrap; + .qr-code { + flex: 1; + padding-right: 10px; + } + .verify { flex: 1; } + .error { margin: 4px 0 0 0; } + .confirm-otp-actions { + button { + width: 15em; + margin-top: 5px; + } + + } + } +} +</style> diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js new file mode 100644 index 00000000..f0a984ec --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/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/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue new file mode 100644 index 00000000..d7e98b3c --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue @@ -0,0 +1,35 @@ +<template> + <div class="mfa-backup-codes"> + <h4 v-if="displayTitle"> + {{ $t('settings.mfa.recovery_codes') }} + </h4> + <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i> + <template v-if="ready"> + <p class="alert warning"> + {{ $t('settings.mfa.recovery_codes_warning') }} + </p> + <ul class="backup-codes"> + <li + v-for="code in backupCodes.codes" + :key="code" + > + {{ code }} + </li> + </ul> + </template> + </div> +</template> +<script src="./mfa_backup_codes.js"></script> +<style lang="scss"> +@import '../../../../_variables.scss'; + +.mfa-backup-codes { + .warning { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + .backup-codes { + font-family: var(--postCodeFont, monospace); + } +} +</style> diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js new file mode 100644 index 00000000..8408d8e9 --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/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/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue new file mode 100644 index 00000000..c6f2cc7b --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue @@ -0,0 +1,43 @@ +<template> + <div> + <div class="method-item"> + <strong>{{ $t('settings.mfa.otp') }}</strong> + <button + v-if="!isActivated" + class="btn btn-default" + @click="doActivate" + > + {{ $t('general.enable') }} + </button> + + <button + v-if="isActivated" + class="btn btn-default" + :disabled="deactivate" + @click="doDeactivate" + > + {{ $t('general.disable') }} + </button> + </div> + + <confirm + v-if="deactivate" + :disabled="inProgress" + @confirm="confirmDeactivate" + @cancel="cancelDeactivate" + > + {{ $t('settings.enter_current_password_to_confirm') }}: + <input + v-model="currentPassword" + type="password" + > + </confirm> + <div + v-if="error" + class="alert error" + > + {{ error }} + </div> + </div> +</template> +<script src="./mfa_totp.js"></script> diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js new file mode 100644 index 00000000..811161a5 --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/security_tab.js @@ -0,0 +1,106 @@ +import ProgressButton from 'src/components/progress_button/progress_button.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Mfa from './mfa.vue' + +const SecurityTab = { + data () { + return { + newEmail: '', + changeEmailError: false, + changeEmailPassword: '', + changedEmail: false, + deletingAccount: false, + deleteAccountConfirmPasswordInput: '', + deleteAccountError: false, + changePasswordInputs: [ '', '', '' ], + changedPassword: false, + changePasswordError: false + } + }, + created () { + this.$store.dispatch('fetchTokens') + }, + components: { + ProgressButton, + Mfa, + Checkbox + }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + pleromaBackend () { + return this.$store.state.instance.pleromaBackend + }, + oauthTokens () { + return this.$store.state.oauthTokens.tokens.map(oauthToken => { + return { + id: oauthToken.id, + appName: oauthToken.app_name, + validUntil: new Date(oauthToken.valid_until).toLocaleDateString() + } + }) + } + }, + methods: { + confirmDelete () { + this.deletingAccount = true + }, + deleteAccount () { + this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput }) + .then((res) => { + if (res.status === 'success') { + this.$store.dispatch('logout') + this.$router.push({ name: 'root' }) + } else { + this.deleteAccountError = res.error + } + }) + }, + changePassword () { + const params = { + password: this.changePasswordInputs[0], + newPassword: this.changePasswordInputs[1], + newPasswordConfirmation: this.changePasswordInputs[2] + } + this.$store.state.api.backendInteractor.changePassword(params) + .then((res) => { + if (res.status === 'success') { + this.changedPassword = true + this.changePasswordError = false + this.logout() + } else { + this.changedPassword = false + this.changePasswordError = res.error + } + }) + }, + changeEmail () { + const params = { + email: this.newEmail, + password: this.changeEmailPassword + } + this.$store.state.api.backendInteractor.changeEmail(params) + .then((res) => { + if (res.status === 'success') { + this.changedEmail = true + this.changeEmailError = false + } else { + this.changedEmail = false + this.changeEmailError = res.error + } + }) + }, + logout () { + this.$store.dispatch('logout') + this.$router.replace('/') + }, + revokeToken (id) { + if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) { + this.$store.dispatch('revokeToken', id) + } + } + } +} + +export default SecurityTab diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue new file mode 100644 index 00000000..3d32d73d --- /dev/null +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -0,0 +1,143 @@ +<template> + <div :label="$t('settings.security_tab')"> + <div class="setting-item"> + <h2>{{ $t('settings.change_email') }}</h2> + <div> + <p>{{ $t('settings.new_email') }}</p> + <input + v-model="newEmail" + type="email" + autocomplete="email" + > + </div> + <div> + <p>{{ $t('settings.current_password') }}</p> + <input + v-model="changeEmailPassword" + type="password" + autocomplete="current-password" + > + </div> + <button + class="btn btn-default" + @click="changeEmail" + > + {{ $t('general.submit') }} + </button> + <p v-if="changedEmail"> + {{ $t('settings.changed_email') }} + </p> + <template v-if="changeEmailError !== false"> + <p>{{ $t('settings.change_email_error') }}</p> + <p>{{ changeEmailError }}</p> + </template> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.change_password') }}</h2> + <div> + <p>{{ $t('settings.current_password') }}</p> + <input + v-model="changePasswordInputs[0]" + type="password" + > + </div> + <div> + <p>{{ $t('settings.new_password') }}</p> + <input + v-model="changePasswordInputs[1]" + type="password" + > + </div> + <div> + <p>{{ $t('settings.confirm_new_password') }}</p> + <input + v-model="changePasswordInputs[2]" + type="password" + > + </div> + <button + class="btn btn-default" + @click="changePassword" + > + {{ $t('general.submit') }} + </button> + <p v-if="changedPassword"> + {{ $t('settings.changed_password') }} + </p> + <p v-else-if="changePasswordError !== false"> + {{ $t('settings.change_password_error') }} + </p> + <p v-if="changePasswordError"> + {{ changePasswordError }} + </p> + </div> + + <div class="setting-item"> + <h2>{{ $t('settings.oauth_tokens') }}</h2> + <table class="oauth-tokens"> + <thead> + <tr> + <th>{{ $t('settings.app_name') }}</th> + <th>{{ $t('settings.valid_until') }}</th> + <th /> + </tr> + </thead> + <tbody> + <tr + v-for="oauthToken in oauthTokens" + :key="oauthToken.id" + > + <td>{{ oauthToken.appName }}</td> + <td>{{ oauthToken.validUntil }}</td> + <td class="actions"> + <button + class="btn btn-default" + @click="revokeToken(oauthToken.id)" + > + {{ $t('settings.revoke_token') }} + </button> + </td> + </tr> + </tbody> + </table> + </div> + <mfa /> + <div class="setting-item"> + <h2>{{ $t('settings.delete_account') }}</h2> + <p v-if="!deletingAccount"> + {{ $t('settings.delete_account_description') }} + </p> + <div v-if="deletingAccount"> + <p>{{ $t('settings.delete_account_instructions') }}</p> + <p>{{ $t('login.password') }}</p> + <input + v-model="deleteAccountConfirmPasswordInput" + type="password" + > + <button + class="btn btn-default" + @click="deleteAccount" + > + {{ $t('settings.delete_account') }} + </button> + </div> + <p v-if="deleteAccountError !== false"> + {{ $t('settings.delete_account_error') }} + </p> + <p v-if="deleteAccountError"> + {{ deleteAccountError }} + </p> + <button + v-if="!deletingAccount" + class="btn btn-default" + @click="confirmDelete" + > + {{ $t('general.submit') }} + </button> + </div> + </div> +</template> + +<script src="./security_tab.js"></script> +<!-- <style lang="scss" src="./profile.scss"></style> --> |
