Revert "add TOTP/Recovery Form for mobile version"
This reverts commit a3811f944819430c278b6da6b08dc322a9b9ff65.
This commit is contained in:
parent
9df99c5205
commit
77eceedbf7
32 changed files with 1657 additions and 439 deletions
26
src/components/auth_form/auth_form.js
Normal file
26
src/components/auth_form/auth_form.js
Normal file
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,53 @@
|
|||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
<div class="panel-heading">
|
||||
{{$t('login.login')}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">{{$t('login.login')}}</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<template v-if="isPasswordAuth">
|
||||
<div class='form-group'>
|
||||
<label for='username'>{{$t('login.username')}}</label>
|
||||
<input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
|
||||
<input :disabled="loggingIn" v-model='user.username'
|
||||
class='form-control' id='username'
|
||||
:placeholder="$t('login.placeholder')">
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='password'>{{$t('login.password')}}</label>
|
||||
<input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'>
|
||||
<input :disabled="loggingIn" v-model='user.password'
|
||||
ref='passwordInput' class='form-control' id='password' type='password'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form">
|
||||
<div class="form-group">
|
||||
<p>{{$t('login.description')}}</p>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
||||
<div class="form-group" v-if="isTokenAuth">
|
||||
<p>{{$t('login.description')}}</p>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<router-link :to="{name: 'registration'}"
|
||||
v-if='registrationOpen'
|
||||
class='register'>
|
||||
{{$t('login.register')}}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="authError" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{authError}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>
|
||||
{{$t('login.login')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login_form.js" ></script>
|
||||
|
|
41
src/components/mfa_form/recovery_form.js
Normal file
41
src/components/mfa_form/recovery_form.js
Normal file
|
@ -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'})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
42
src/components/mfa_form/recovery_form.vue
Normal file
42
src/components/mfa_form/recovery_form.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">{{$t('login.heading.recovery')}}</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<div class='form-group'>
|
||||
<label for='code'>{{$t('login.recovery_code')}}</label>
|
||||
<input v-model='code' class='form-control' id='code'>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<a href="#" @click.prevent="requireTOTP">
|
||||
{{$t('login.enter_two_factor_code')}}
|
||||
</a>
|
||||
<br />
|
||||
<a href="#" @click.prevent="abortMFA">
|
||||
{{$t('general.cancel')}}
|
||||
</a>
|
||||
</div>
|
||||
<button type='submit' class='btn btn-default'>
|
||||
{{$t('general.verify')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./recovery_form.js" ></script>
|
40
src/components/mfa_form/totp_form.js
Normal file
40
src/components/mfa_form/totp_form.js
Normal file
|
@ -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'})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
45
src/components/mfa_form/totp_form.vue
Normal file
45
src/components/mfa_form/totp_form.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">
|
||||
{{$t('login.heading.totp')}}
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<div class='form-group'>
|
||||
<label for='code'>
|
||||
{{$t('login.authentication_code')}}
|
||||
</label>
|
||||
<input v-model='code' class='form-control' id='code'>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<a href="#" @click.prevent="requireRecovery">
|
||||
{{$t('login.enter_recovery_code')}}
|
||||
</a>
|
||||
<br />
|
||||
<a href="#" @click.prevent="abortMFA">
|
||||
{{$t('general.cancel')}}
|
||||
</a>
|
||||
</div>
|
||||
<button type='submit' class='btn btn-default'>
|
||||
{{$t('general.verify')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./totp_form.js"></script>
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
<template>
|
||||
<div class="user-panel">
|
||||
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
||||
|
||||
<div v-if="signedIn" key="user-panel" class="panel panel-default signed-in">
|
||||
<UserCard :user="user" :hideBio="true" rounded="top"/>
|
||||
<div class="panel-footer">
|
||||
<post-status-form v-if='user'></post-status-form>
|
||||
</div>
|
||||
</div>
|
||||
<login-form v-if='!user'></login-form>
|
||||
<auth-form v-else key="user-panel"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-panel .signed-in {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
|
9
src/components/user_settings/confirm.js
Normal file
9
src/components/user_settings/confirm.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const Confirm = {
|
||||
props: ['disabled'],
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
confirm () { this.$emit('confirm') },
|
||||
cancel () { this.$emit('cancel') }
|
||||
}
|
||||
}
|
||||
export default Confirm
|
14
src/components/user_settings/confirm.vue
Normal file
14
src/components/user_settings/confirm.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<button class="btn btn-default" @click="confirm" :disabled="disabled">
|
||||
{{$t('general.confirm')}}
|
||||
</button>
|
||||
<button class="btn btn-default" @click="cancel" :disabled="disabled">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./confirm.js">
|
||||
</script>
|
152
src/components/user_settings/mfa.js
Normal file
152
src/components/user_settings/mfa.js
Normal file
|
@ -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
|
121
src/components/user_settings/mfa.vue
Normal file
121
src/components/user_settings/mfa.vue
Normal file
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="setting-item mfa-settings" v-if="readyInit">
|
||||
|
||||
<div class="mfa-heading">
|
||||
<h2>{{$t('settings.mfa.title')}}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="setting-item" v-if="!setupInProgress">
|
||||
<!-- 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 :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" />
|
||||
<button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes">
|
||||
{{$t('settings.mfa.generate_new_recovery_codes')}}
|
||||
</button>
|
||||
|
||||
<div v-if="confirmNewBackupCodes">
|
||||
<confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes"
|
||||
:disabled="backupCodes.inProgress">
|
||||
<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 :backup-codes="backupCodes" v-if="!setupOTPInProgress"/>
|
||||
|
||||
|
||||
<button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" v-if="canSetupOTP" @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 }"></qrcode>
|
||||
<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 type="text" v-model="otpConfirmToken">
|
||||
|
||||
<p>{{$t('settings.enter_current_password_to_confirm')}}:</p>
|
||||
<input type="password" v-model="currentPassword">
|
||||
<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 class="alert error" v-if="error">{{error}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mfa.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.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>
|
17
src/components/user_settings/mfa_backup_codes.js
Normal file
17
src/components/user_settings/mfa_backup_codes.js
Normal file
|
@ -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 }
|
||||
}
|
||||
}
|
22
src/components/user_settings/mfa_backup_codes.vue
Normal file
22
src/components/user_settings/mfa_backup_codes.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<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">{{code}}</li></ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
</style>
|
49
src/components/user_settings/mfa_totp.js
Normal file
49
src/components/user_settings/mfa_totp.js
Normal file
|
@ -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')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
23
src/components/user_settings/mfa_totp.vue
Normal file
23
src/components/user_settings/mfa_totp.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="method-item">
|
||||
<strong>{{$t('settings.mfa.otp')}}</strong>
|
||||
<button class="btn btn-default" v-if="!isActivated" @click="doActivate">
|
||||
{{$t('general.enable')}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" :disabled="deactivate" @click="doDeactivate"
|
||||
v-if="isActivated">
|
||||
{{$t('general.disable')}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<confirm @confirm="confirmDeactivate" @cancel="cancelDeactivate"
|
||||
:disabled="inProgress" v-if="deactivate">
|
||||
{{$t('settings.enter_current_password_to_confirm')}}:
|
||||
<input type="password" v-model="currentPassword">
|
||||
</confirm>
|
||||
<div class="alert error" v-if="error">{{error}}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_totp.js"></script>
|
|
@ -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 () {
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mfa />
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.delete_account')}}</h2>
|
||||
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue