moved stuff from settings, cleaned up naming for tabs, added close and peek

This commit is contained in:
Henry Jameson 2020-05-10 06:46:06 +03:00
parent 2e35289c33
commit bcebec478e
42 changed files with 801 additions and 1053 deletions

View file

@ -0,0 +1,9 @@
const Confirm = {
props: ['disabled'],
data: () => ({}),
methods: {
confirm () { this.$emit('confirm') },
cancel () { this.$emit('cancel') }
}
}
export default Confirm

View file

@ -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>

View file

@ -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

View file

@ -0,0 +1,174 @@
<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 {
overflow: hidden;
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>

View 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 }
}
}

View file

@ -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>

View 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')
})
}
}
}

View file

@ -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>

View file

@ -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

View file

@ -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> -->