feat: move user settings to multiple components (#889)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/889 Co-authored-by: konrad <k@knt.li> Co-committed-by: konrad <k@knt.li>
This commit is contained in:
parent
e1a7fb4999
commit
5040a76781
16 changed files with 703 additions and 477 deletions
|
@ -8,7 +8,7 @@ describe('User Settings', () => {
|
|||
})
|
||||
|
||||
it('Changes the user avatar', () => {
|
||||
cy.visit('/user/settings')
|
||||
cy.visit('/user/settings/avatar')
|
||||
|
||||
cy.get('input[name=avatarProvider][value=upload]')
|
||||
.click()
|
||||
|
@ -28,9 +28,10 @@ describe('User Settings', () => {
|
|||
})
|
||||
|
||||
it('Updates the name', () => {
|
||||
cy.visit('/user/settings')
|
||||
cy.visit('/user/settings/general')
|
||||
|
||||
cy.get('input#newName')
|
||||
cy.get('.general-settings .control input.input')
|
||||
.first()
|
||||
.type('Lorem Ipsum')
|
||||
cy.get('.card.general-settings .button.is-primary')
|
||||
.contains('Save')
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank" rel="noreferrer noopener nofollow">
|
||||
<a class="menu-bottom-link" :href="poweredByUrl" target="_blank" rel="noreferrer noopener nofollow">
|
||||
{{ $t('misc.poweredBy') }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -160,6 +160,7 @@ import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
|||
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {POWERED_BY} from '@/urls'
|
||||
|
||||
import logoUrl from '@/assets/logo-full.svg'
|
||||
|
||||
|
@ -175,6 +176,7 @@ export default {
|
|||
},
|
||||
listUpdating: {},
|
||||
logoUrl,
|
||||
poweredByUrl: POWERED_BY,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -48,6 +48,14 @@ import CreateSavedFilter from '../views/filters/CreateSavedFilter'
|
|||
const PasswordResetComponent = () => import('../views/user/PasswordReset')
|
||||
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset')
|
||||
const UserSettingsComponent = () => import('../views/user/Settings')
|
||||
const UserSettingsAvatarComponent = () => import('../views/user/settings/Avatar')
|
||||
const UserSettingsCaldavComponent = () => import('../views/user/settings/Caldav')
|
||||
const UserSettingsDataExportComponent = () => import('../views/user/settings/DataExport')
|
||||
const UserSettingsDeletionComponent = () => import('../views/user/settings/Deletion')
|
||||
const UserSettingsEmailUpdateComponent = () => import('../views/user/settings/EmailUpdate')
|
||||
const UserSettingsGeneralComponent = () => import('../views/user/settings/General')
|
||||
const UserSettingsPasswordUpdateComponent = () => import('../views/user/settings/PasswordUpdate')
|
||||
const UserSettingsTOTPComponent = () => import('../views/user/settings/TOTP')
|
||||
|
||||
// List Handling
|
||||
const NewListComponent = () => import('../views/list/NewList')
|
||||
|
@ -115,6 +123,48 @@ const router = createRouter({
|
|||
path: '/user/settings',
|
||||
name: 'user.settings',
|
||||
component: UserSettingsComponent,
|
||||
children: [
|
||||
{
|
||||
path: '/user/settings/avatar',
|
||||
name: 'user.settings.avatar',
|
||||
component: UserSettingsAvatarComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/caldav',
|
||||
name: 'user.settings.caldav',
|
||||
component: UserSettingsCaldavComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/data-export',
|
||||
name: 'user.settings.data-export',
|
||||
component: UserSettingsDataExportComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/deletion',
|
||||
name: 'user.settings.deletion',
|
||||
component: UserSettingsDeletionComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/email-update',
|
||||
name: 'user.settings.email-update',
|
||||
component: UserSettingsEmailUpdateComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/general',
|
||||
name: 'user.settings.general',
|
||||
component: UserSettingsGeneralComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/password-update',
|
||||
name: 'user.settings.password-update',
|
||||
component: UserSettingsPasswordUpdateComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/totp',
|
||||
name: 'user.settings.totp',
|
||||
component: UserSettingsTOTPComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/user/export/download',
|
||||
|
|
|
@ -2,6 +2,7 @@ import {CONFIG} from '../mutation-types'
|
|||
import {HTTPFactory} from '@/http-common'
|
||||
import {objectToCamelCase} from '@/helpers/case'
|
||||
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
||||
import {parseURL} from 'ufo'
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
@ -35,6 +36,13 @@ export default {
|
|||
},
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
migratorsEnabled: state => state.availableMigrators?.length > 0,
|
||||
apiBase() {
|
||||
const {host, protocol} = parseURL(window.API_URL)
|
||||
return protocol + '//' + host
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[CONFIG](state, config) {
|
||||
state.version = config.version
|
||||
|
@ -63,7 +71,7 @@ export default {
|
|||
async update(ctx) {
|
||||
const HTTP = HTTPFactory()
|
||||
|
||||
const { data: info } = await HTTP.get('info')
|
||||
const {data: info} = await HTTP.get('info')
|
||||
ctx.commit(CONFIG, info)
|
||||
return info
|
||||
},
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
.content h3 {
|
||||
.icon,
|
||||
.is-small {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.icon,
|
||||
.is-small {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table.has-actions {
|
||||
border-top: 1px solid $grey-100;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $grey-100;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.actions {
|
||||
text-align: right;
|
||||
}
|
||||
td.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.content-widescreen {
|
||||
margin: 0 auto;
|
||||
max-width: $widescreen;
|
||||
}
|
||||
|
||||
|
|
2
src/urls.js
Normal file
2
src/urls.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const POWERED_BY = 'https://vikunja.io'
|
||||
export const CALDAV_DOCS = 'https://vikunja.io/docs/caldav/'
|
|
@ -43,6 +43,7 @@
|
|||
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
autocomplete="one-time-code"
|
||||
class="input"
|
||||
id="totpPasscode"
|
||||
:placeholder="$t('user.auth.totpPlaceholder')"
|
||||
|
|
|
@ -1,469 +1,116 @@
|
|||
<template>
|
||||
<div
|
||||
:class="{ 'is-loading': passwordUpdateService.loading || emailUpdateService.loading || totpService.loading }"
|
||||
class="loader-container is-max-width-desktop">
|
||||
<!-- General -->
|
||||
<card :title="$t('user.settings.general.title')" class="general-settings">
|
||||
<div class="field">
|
||||
<label class="label" for="newName">{{ $t('user.settings.general.name') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updateSettings"
|
||||
class="input"
|
||||
id="newName"
|
||||
:placeholder="$t('user.settings.general.newName')"
|
||||
type="text"
|
||||
v-model="settings.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{{ $t('user.settings.general.defaultList') }}
|
||||
</label>
|
||||
<list-search v-model="defaultList"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
|
||||
{{ $t('user.settings.general.emailReminders') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
|
||||
{{ $t('user.settings.general.overdueReminders') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.discoverableByName"/>
|
||||
{{ $t('user.settings.general.discoverableByName') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.discoverableByEmail"/>
|
||||
{{ $t('user.settings.general.discoverableByEmail') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="playSoundWhenDone"/>
|
||||
{{ $t('user.settings.general.playSoundWhenDone') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
<span>
|
||||
{{ $t('user.settings.general.weekStart') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model.number="settings.weekStart">
|
||||
<option value="0">{{ $t('user.settings.general.weekStartSunday') }}</option>
|
||||
<option value="1">{{ $t('user.settings.general.weekStartMonday') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
<span>
|
||||
{{ $t('user.settings.general.language') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model="language">
|
||||
<option :value="lang.code" v-for="lang in availableLanguages" :key="lang.code">{{ lang.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
<span>
|
||||
{{ $t('user.settings.quickAddMagic.title') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model="quickAddMagicMode">
|
||||
<option v-for="set in quickAddMagicPrefixes" :key="set" :value="set">{{ $t(`user.settings.quickAddMagic.${set}`) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
:loading="userSettingsService.loading"
|
||||
@click="updateSettings()"
|
||||
class="is-fullwidth mt-4"
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</card>
|
||||
|
||||
<!-- Avatar -->
|
||||
<avatar-settings/>
|
||||
|
||||
<!-- Password update -->
|
||||
<card :title="$t('user.settings.newPasswordTitle')">
|
||||
<form @submit.prevent="updatePassword()">
|
||||
<div class="field">
|
||||
<label class="label" for="newPassword">{{ $t('user.settings.newPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updatePassword"
|
||||
class="input"
|
||||
id="newPassword"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
type="password"
|
||||
v-model="passwordUpdate.newPassword"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="newPasswordConfirm">{{ $t('user.settings.newPasswordConfirm') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updatePassword"
|
||||
class="input"
|
||||
id="newPasswordConfirm"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
type="password"
|
||||
v-model="passwordConfirm"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPassword">{{ $t('user.settings.currentPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updatePassword"
|
||||
class="input"
|
||||
id="currentPassword"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="passwordUpdate.oldPassword"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-button
|
||||
:loading="passwordUpdateService.loading"
|
||||
@click="updatePassword()"
|
||||
class="is-fullwidth mt-4">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</card>
|
||||
|
||||
<!-- Update E-Mail -->
|
||||
<card :title="$t('user.settings.updateEmailTitle')">
|
||||
<form @submit.prevent="updateEmail()">
|
||||
<div class="field">
|
||||
<label class="label" for="newEmail">{{ $t('user.settings.updateEmailNew') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updateEmail"
|
||||
class="input"
|
||||
id="newEmail"
|
||||
:placeholder="$t('user.auth.emailPlaceholder')"
|
||||
type="email"
|
||||
v-model="emailUpdate.newEmail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPasswordEmail">{{ $t('user.settings.currentPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updateEmail"
|
||||
class="input"
|
||||
id="currentPasswordEmail"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="emailUpdate.password"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-button
|
||||
:loading="emailUpdateService.loading"
|
||||
@click="updateEmail()"
|
||||
class="is-fullwidth mt-4">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</card>
|
||||
|
||||
<!-- TOTP -->
|
||||
<card :title="$t('user.settings.totp.title')" v-if="totpEnabled">
|
||||
<x-button
|
||||
:loading="totpService.loading"
|
||||
@click="totpEnroll()"
|
||||
v-if="!totpEnrolled && totp.secret === ''">
|
||||
{{ $t('user.settings.totp.enroll') }}
|
||||
</x-button>
|
||||
<template v-else-if="totp.secret !== '' && !totp.enabled">
|
||||
<p>
|
||||
{{ $t('user.settings.totp.finishSetupPart1') }}
|
||||
<strong>{{ totp.secret }}</strong><br/>
|
||||
{{ $t('user.settings.totp.finishSetupPart2') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('user.settings.totp.scanQR') }}<br/>
|
||||
<img :src="totpQR" alt=""/>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="totpConfirmPasscode">{{ $t('user.settings.totp.passcode') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="totpConfirm()"
|
||||
class="input"
|
||||
id="totpConfirmPasscode"
|
||||
:placeholder="$t('user.settings.totp.passcodePlaceholder')"
|
||||
type="text"
|
||||
v-model="totpConfirmPasscode"/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="totpConfirm()">{{ $t('misc.confirm') }}</x-button>
|
||||
</template>
|
||||
<template v-else-if="totp.secret !== '' && totp.enabled">
|
||||
<p>
|
||||
{{ $t('user.settings.totp.setupSuccess') }}
|
||||
</p>
|
||||
<p v-if="!totpDisableForm">
|
||||
<x-button @click="totpDisableForm = true" class="is-danger">{{ $t('misc.disable') }}</x-button>
|
||||
</p>
|
||||
<div v-if="totpDisableForm">
|
||||
<div class="field">
|
||||
<label class="label" for="currentPassword">{{ $t('user.settings.totp.enterPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="totpDisable"
|
||||
class="input"
|
||||
id="currentPassword"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-focus
|
||||
v-model="totpDisablePassword"/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="totpDisable()" class="is-danger">
|
||||
{{ $t('user.settings.totp.disable') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</template>
|
||||
</card>
|
||||
|
||||
<!-- Data export -->
|
||||
<data-export/>
|
||||
|
||||
<!-- Migration -->
|
||||
<card :title="$t('migrate.title')" v-if="migratorsEnabled">
|
||||
<x-button
|
||||
:to="{name: 'migrate.start'}"
|
||||
>
|
||||
{{ $t('migrate.import') }}
|
||||
</x-button>
|
||||
</card>
|
||||
|
||||
<!-- Account deletion -->
|
||||
<user-settings-deletion id="deletion"/>
|
||||
|
||||
<!-- Caldav -->
|
||||
<card v-if="caldavEnabled" :title="$t('user.settings.caldav.title')">
|
||||
<p>
|
||||
{{ $t('user.settings.caldav.howTo') }}
|
||||
</p>
|
||||
<div class="field has-addons no-input-mobile">
|
||||
<div class="control is-expanded">
|
||||
<input type="text" v-model="caldavUrl" class="input" readonly/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
@click="copy(caldavUrl)"
|
||||
:shadow="false"
|
||||
v-tooltip="$t('misc.copy')"
|
||||
icon="paste"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<a href="https://vikunja.io/docs/caldav/" rel="noreferrer noopener nofollow" target="_blank">
|
||||
{{ $t('user.settings.caldav.more') }}
|
||||
</a>
|
||||
</p>
|
||||
</card>
|
||||
<div class="content-widescreen">
|
||||
<div class="user-settings">
|
||||
<nav class="navigation">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{name: 'user.settings.general'}">
|
||||
{{ $t('user.settings.general.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'user.settings.password-update'}">
|
||||
{{ $t('user.settings.newPasswordTitle') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'user.settings.email-update'}">
|
||||
{{ $t('user.settings.updateEmailTitle') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'user.settings.avatar'}">
|
||||
{{ $t('user.settings.avatar.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="totpEnabled">
|
||||
<router-link :to="{name: 'user.settings.totp'}">
|
||||
{{ $t('user.settings.totp.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'user.settings.data-export'}">
|
||||
{{ $t('user.export.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="migratorsEnabled">
|
||||
<router-link :to="{name: 'migrate.start'}">
|
||||
{{ $t('migrate.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="caldavEnabled">
|
||||
<router-link :to="{name: 'user.settings.caldav'}">
|
||||
{{ $t('user.settings.caldav.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'user.settings.deletion'}">
|
||||
{{ $t('user.deletion.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="view">
|
||||
<router-view/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordUpdateModel from '../../models/passwordUpdate'
|
||||
import PasswordUpdateService from '../../services/passwordUpdateService'
|
||||
import EmailUpdateService from '../../services/emailUpdate'
|
||||
import EmailUpdateModel from '../../models/emailUpdate'
|
||||
import TotpModel from '../../models/totp'
|
||||
import TotpService from '../../services/totp'
|
||||
import UserSettingsService from '../../services/userSettings'
|
||||
import {playSoundWhenDoneKey} from '@/helpers/playPop'
|
||||
import {availableLanguages, saveLanguage, getCurrentLanguage} from '@/i18n'
|
||||
import {getQuickAddMagicMode, setQuickAddMagicMode} from '../../helpers/quickAddMagicMode'
|
||||
import {PrefixMode} from '../../modules/parseTaskText'
|
||||
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import AvatarSettings from '../../components/user/avatar-settings.vue'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import ListSearch from '@/components/tasks/partials/listSearch.vue'
|
||||
import UserSettingsDeletion from '../../components/user/settings/deletion'
|
||||
import DataExport from '../../components/user/settings/data-export'
|
||||
|
||||
function getPlaySoundWhenDoneSetting() {
|
||||
return localStorage.getItem(playSoundWhenDoneKey) === 'true' || localStorage.getItem(playSoundWhenDoneKey) === null
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
data() {
|
||||
return {
|
||||
passwordUpdateService: new PasswordUpdateService(),
|
||||
passwordUpdate: new PasswordUpdateModel(),
|
||||
passwordConfirm: '',
|
||||
|
||||
emailUpdateService: new EmailUpdateService(),
|
||||
emailUpdate: new EmailUpdateModel(),
|
||||
|
||||
totpService: new TotpService(),
|
||||
totp: new TotpModel(),
|
||||
totpQR: '',
|
||||
totpEnrolled: false,
|
||||
totpConfirmPasscode: '',
|
||||
totpDisableForm: false,
|
||||
totpDisablePassword: '',
|
||||
playSoundWhenDone: getPlaySoundWhenDoneSetting(),
|
||||
language: getCurrentLanguage(),
|
||||
quickAddMagicMode: getQuickAddMagicMode(),
|
||||
quickAddMagicPrefixes: PrefixMode,
|
||||
|
||||
settings: { ...this.$store.state.auth.settings },
|
||||
userSettingsService: new UserSettingsService(),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserSettingsDeletion,
|
||||
ListSearch,
|
||||
AvatarSettings,
|
||||
DataExport,
|
||||
},
|
||||
created() {
|
||||
this.totpStatus()
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(this.$t('user.settings.title'))
|
||||
this.anchorHashCheck()
|
||||
},
|
||||
computed: {
|
||||
defaultList() {
|
||||
return this.$store.getters['lists/getListById'](this.settings.defaultListId)
|
||||
},
|
||||
caldavUrl() {
|
||||
let apiBase = window.API_URL.replace('/api/v1', '')
|
||||
if (apiBase === '') { // Frontend and api on the same host which means we need to prefix the frontend url
|
||||
apiBase = this.$store.state.config.frontendUrl
|
||||
}
|
||||
if (apiBase.endsWith('/')) {
|
||||
apiBase = apiBase.substr(0, apiBase.length - 1)
|
||||
}
|
||||
|
||||
return `${apiBase}/dav/principals/${this.userInfo.username}/`
|
||||
},
|
||||
availableLanguages() {
|
||||
return Object.entries(availableLanguages)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a, b) => a.title > b.title)
|
||||
},
|
||||
...mapState({
|
||||
totpEnabled: state => state.config.totpEnabled,
|
||||
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||
caldavEnabled: state => state.config.caldavEnabled,
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
copy,
|
||||
|
||||
async updatePassword() {
|
||||
if (this.passwordConfirm !== this.passwordUpdate.newPassword) {
|
||||
this.$message.error({message: this.$t('user.settings.passwordsDontMatch')})
|
||||
return
|
||||
}
|
||||
|
||||
await this.passwordUpdateService.update(this.passwordUpdate)
|
||||
this.$message.success({message: this.$t('user.settings.passwordUpdateSuccess')})
|
||||
},
|
||||
|
||||
async updateEmail() {
|
||||
await this.emailUpdateService.update(this.emailUpdate)
|
||||
this.$message.success({message: this.$t('user.settings.updateEmailSuccess')})
|
||||
},
|
||||
|
||||
async totpStatus() {
|
||||
if (!this.totpEnabled) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.totp = await this.totpService.get()
|
||||
this.totpSetQrCode()
|
||||
} catch(e) {
|
||||
// Error code 1016 means totp is not enabled, we don't need an error in that case.
|
||||
if (e.response && e.response.data && e.response.data.code && e.response.data.code === 1016) {
|
||||
this.totpEnrolled = false
|
||||
return
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
},
|
||||
|
||||
async totpSetQrCode() {
|
||||
const qr = await this.totpService.qrcode()
|
||||
const urlCreator = window.URL || window.webkitURL
|
||||
this.totpQR = urlCreator.createObjectURL(qr)
|
||||
},
|
||||
|
||||
async totpEnroll() {
|
||||
this.totp = await this.totpService.enroll()
|
||||
this.totpEnrolled = true
|
||||
this.totpSetQrCode()
|
||||
},
|
||||
|
||||
async totpConfirm() {
|
||||
await this.totpService.enable({passcode: this.totpConfirmPasscode})
|
||||
this.totp.enabled = true
|
||||
this.$message.success({message: this.$t('user.settings.totp.confirmSuccess')})
|
||||
},
|
||||
|
||||
async totpDisable() {
|
||||
await this.totpService.disable({password: this.totpDisablePassword})
|
||||
this.totpEnrolled = false
|
||||
this.totp = new TotpModel()
|
||||
this.$message.success({message: this.$t('user.settings.totp.disableSuccess')})
|
||||
},
|
||||
|
||||
async updateSettings() {
|
||||
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
|
||||
saveLanguage(this.language)
|
||||
setQuickAddMagicMode(this.quickAddMagicMode)
|
||||
this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0
|
||||
|
||||
await this.userSettingsService.update(this.settings)
|
||||
this.$store.commit('auth/setUserSettings', {
|
||||
...this.settings,
|
||||
})
|
||||
this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
|
||||
},
|
||||
|
||||
anchorHashCheck() {
|
||||
if (window.location.hash === this.$route.hash) {
|
||||
const el = document.getElementById(this.$route.hash.slice(1))
|
||||
if (el) {
|
||||
window.scrollTo(0, el.offsetTop)
|
||||
}
|
||||
}
|
||||
...mapState('config', ['totpEnabled', 'caldavEnabled']),
|
||||
migratorsEnabled() {
|
||||
return this.$store.getters['config/migratorsEnabled']
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-settings {
|
||||
display: flex;
|
||||
|
||||
.navigation {
|
||||
width: 25%;
|
||||
padding-right: 1rem;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: .5rem;
|
||||
color: $dark;
|
||||
width: 100%;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover, &.router-link-active {
|
||||
background: $white;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
flex-direction: column;
|
||||
|
||||
.navigation, .view {
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.view {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -66,11 +66,11 @@
|
|||
import {Cropper} from 'vue-advanced-cropper'
|
||||
import 'vue-advanced-cropper/dist/style.css'
|
||||
|
||||
import AvatarService from '../../services/avatar'
|
||||
import AvatarModel from '../../models/avatar'
|
||||
import AvatarService from '@/services/avatar'
|
||||
import AvatarModel from '@/models/avatar'
|
||||
|
||||
export default {
|
||||
name: 'avatar-settings',
|
||||
name: 'user-settings-avatar',
|
||||
data() {
|
||||
return {
|
||||
avatarProvider: '',
|
||||
|
@ -86,6 +86,9 @@ export default {
|
|||
components: {
|
||||
Cropper,
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.settings.avatar.title')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
methods: {
|
||||
async avatarStatus() {
|
||||
const { avatarProvider } = await this.avatarService.get({})
|
55
src/views/user/settings/Caldav.vue
Normal file
55
src/views/user/settings/Caldav.vue
Normal file
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<card v-if="caldavEnabled" :title="$t('user.settings.caldav.title')">
|
||||
<p>
|
||||
{{ $t('user.settings.caldav.howTo') }}
|
||||
</p>
|
||||
<div class="field has-addons no-input-mobile">
|
||||
<div class="control is-expanded">
|
||||
<input type="text" v-model="caldavUrl" class="input" readonly/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
@click="copy(caldavUrl)"
|
||||
:shadow="false"
|
||||
v-tooltip="$t('misc.copy')"
|
||||
icon="paste"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<a href="https://vikunja.io/docs/caldav/" rel="noreferrer noopener nofollow" target="_blank">
|
||||
{{ $t('user.settings.caldav.more') }}
|
||||
</a>
|
||||
</p>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import copy from 'copy-to-clipboard'
|
||||
import {mapState} from 'vuex'
|
||||
import {CALDAV_DOCS} from '@/urls'
|
||||
|
||||
export default {
|
||||
name: 'user-settings-caldav',
|
||||
data() {
|
||||
return {
|
||||
caldavDocsUrl: CALDAV_DOCS,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.settings.caldav.title')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
computed: {
|
||||
caldavUrl() {
|
||||
return `${this.$store.getters['config/apiBase']}/dav/principals/${this.userInfo.username}/`
|
||||
},
|
||||
...mapState('config', ['caldavEnabled']),
|
||||
...mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
copy,
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -37,10 +37,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import DataExportService from '../../../services/dataExport'
|
||||
import DataExportService from '@/services/dataExport'
|
||||
|
||||
export default {
|
||||
name: 'data-export',
|
||||
name: 'user-settings-data-export',
|
||||
data() {
|
||||
return {
|
||||
dataExportService: new DataExportService(),
|
||||
|
@ -48,6 +48,9 @@ export default {
|
|||
errPasswordRequired: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.export.title')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
methods: {
|
||||
async requestDataExport() {
|
||||
if (this.password === '') {
|
|
@ -83,9 +83,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import AccountDeleteService from '../../../services/accountDelete'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {mapState} from 'vuex'
|
||||
import {parseDateOrNull} from '../../../helpers/parseDateOrNull'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
|
||||
export default {
|
||||
name: 'user-settings-deletion',
|
||||
|
@ -100,6 +100,9 @@ export default {
|
|||
userDeletionEnabled: state => state.config.userDeletionEnabled,
|
||||
deletionScheduledAt: state => parseDateOrNull(state.auth.info.deletionScheduledAt),
|
||||
}),
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.deletion.title')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
methods: {
|
||||
async deleteAccount() {
|
||||
if (this.password === '') {
|
61
src/views/user/settings/EmailUpdate.vue
Normal file
61
src/views/user/settings/EmailUpdate.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<card :title="$t('user.settings.updateEmailTitle')">
|
||||
<form @submit.prevent="updateEmail">
|
||||
<div class="field">
|
||||
<label class="label" for="newEmail">{{ $t('user.settings.updateEmailNew') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updateEmail"
|
||||
class="input"
|
||||
id="newEmail"
|
||||
:placeholder="$t('user.auth.emailPlaceholder')"
|
||||
type="email"
|
||||
v-model="emailUpdate.newEmail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPasswordEmail">{{ $t('user.settings.currentPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updateEmail"
|
||||
class="input"
|
||||
id="currentPasswordEmail"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="emailUpdate.password"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-button
|
||||
:loading="emailUpdateService.loading"
|
||||
@click="updateEmail"
|
||||
class="is-fullwidth mt-4">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmailUpdateService from '@/services/emailUpdate'
|
||||
import EmailUpdateModel from '@/models/emailUpdate'
|
||||
|
||||
export default {
|
||||
name: 'user-settings-update-email',
|
||||
data() {
|
||||
return {
|
||||
emailUpdateService: new EmailUpdateService(),
|
||||
emailUpdate: new EmailUpdateModel(),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.settings.updateEmailTitle')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
methods: {
|
||||
async updateEmail() {
|
||||
await this.emailUpdateService.update(this.emailUpdate)
|
||||
this.$message.success({message: this.$t('user.settings.updateEmailSuccess')})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
169
src/views/user/settings/General.vue
Normal file
169
src/views/user/settings/General.vue
Normal file
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<card :title="$t('user.settings.general.title')" class="general-settings" :loading="userSettingsService.loading">
|
||||
<div class="field">
|
||||
<label class="label" :for="`newName${id}`">{{ $t('user.settings.general.name') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="updateSettings"
|
||||
class="input"
|
||||
:id="`newName${id}`"
|
||||
:placeholder="$t('user.settings.general.newName')"
|
||||
type="text"
|
||||
v-model="settings.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{{ $t('user.settings.general.defaultList') }}
|
||||
</label>
|
||||
<list-search v-model="defaultList"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
|
||||
{{ $t('user.settings.general.emailReminders') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
|
||||
{{ $t('user.settings.general.overdueReminders') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.discoverableByName"/>
|
||||
{{ $t('user.settings.general.discoverableByName') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.discoverableByEmail"/>
|
||||
{{ $t('user.settings.general.discoverableByEmail') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="playSoundWhenDone"/>
|
||||
{{ $t('user.settings.general.playSoundWhenDone') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
<span>
|
||||
{{ $t('user.settings.general.weekStart') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model.number="settings.weekStart">
|
||||
<option value="0">{{ $t('user.settings.general.weekStartSunday') }}</option>
|
||||
<option value="1">{{ $t('user.settings.general.weekStartMonday') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
<span>
|
||||
{{ $t('user.settings.general.language') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model="language">
|
||||
<option :value="lang.code" v-for="lang in availableLanguages" :key="lang.code">{{
|
||||
lang.title
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="is-flex is-align-items-center">
|
||||
<span>
|
||||
{{ $t('user.settings.quickAddMagic.title') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model="quickAddMagicMode">
|
||||
<option v-for="set in quickAddMagicPrefixes" :key="set" :value="set">
|
||||
{{ $t(`user.settings.quickAddMagic.${set}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
:loading="userSettingsService.loading"
|
||||
@click="updateSettings()"
|
||||
class="is-fullwidth mt-4"
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {playSoundWhenDoneKey} from '@/helpers/playPop'
|
||||
import {availableLanguages, saveLanguage, getCurrentLanguage} from '@/i18n'
|
||||
import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
import UserSettingsService from '@/services/userSettings'
|
||||
import {PrefixMode} from '@/modules/parseTaskText'
|
||||
import ListSearch from '@/components/tasks/partials/listSearch'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
|
||||
function getPlaySoundWhenDoneSetting() {
|
||||
return localStorage.getItem(playSoundWhenDoneKey) === 'true' || localStorage.getItem(playSoundWhenDoneKey) === null
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'user-settings-general',
|
||||
data() {
|
||||
return {
|
||||
playSoundWhenDone: getPlaySoundWhenDoneSetting(),
|
||||
language: getCurrentLanguage(),
|
||||
quickAddMagicMode: getQuickAddMagicMode(),
|
||||
quickAddMagicPrefixes: PrefixMode,
|
||||
userSettingsService: new UserSettingsService(),
|
||||
settings: {...this.$store.state.auth.settings},
|
||||
id: createRandomID(),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListSearch,
|
||||
},
|
||||
computed: {
|
||||
availableLanguages() {
|
||||
return Object.entries(availableLanguages)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
},
|
||||
defaultList() {
|
||||
return this.$store.getters['lists/getListById'](this.settings.defaultListId)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.settings.general.title')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
watch: {
|
||||
playSoundWhenDone(play) {
|
||||
if (play) {
|
||||
playPop()
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateSettings() {
|
||||
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
|
||||
saveLanguage(this.language)
|
||||
setQuickAddMagicMode(this.quickAddMagicMode)
|
||||
this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0
|
||||
|
||||
await this.userSettingsService.update(this.settings)
|
||||
this.$store.commit('auth/setUserSettings', {
|
||||
...this.settings,
|
||||
})
|
||||
this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
82
src/views/user/settings/PasswordUpdate.vue
Normal file
82
src/views/user/settings/PasswordUpdate.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<card :title="$t('user.settings.newPasswordTitle')" :loading="passwordUpdateService.loading">
|
||||
<form @submit.prevent="updatePassword">
|
||||
<div class="field">
|
||||
<label class="label" for="newPassword">{{ $t('user.settings.newPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
@keyup.enter="updatePassword"
|
||||
class="input"
|
||||
id="newPassword"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
type="password"
|
||||
v-model="passwordUpdate.newPassword"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="newPasswordConfirm">{{ $t('user.settings.newPasswordConfirm') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
autocomplete="new-password"
|
||||
@keyup.enter="updatePassword"
|
||||
class="input"
|
||||
id="newPasswordConfirm"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
type="password"
|
||||
v-model="passwordConfirm"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPassword">{{ $t('user.settings.currentPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="updatePassword"
|
||||
class="input"
|
||||
id="currentPassword"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="passwordUpdate.oldPassword"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-button
|
||||
:loading="passwordUpdateService.loading"
|
||||
@click="updatePassword"
|
||||
class="is-fullwidth mt-4">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordUpdateService from '@/services/passwordUpdateService'
|
||||
import PasswordUpdateModel from '@/models/passwordUpdate'
|
||||
|
||||
export default {
|
||||
name: 'user-settings-password-update',
|
||||
data() {
|
||||
return {
|
||||
passwordUpdateService: new PasswordUpdateService(),
|
||||
passwordUpdate: new PasswordUpdateModel(),
|
||||
passwordConfirm: '',
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.settings.newPasswordTitle')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
methods: {
|
||||
async updatePassword() {
|
||||
if (this.passwordConfirm !== this.passwordUpdate.newPassword) {
|
||||
this.$message.error({message: this.$t('user.settings.passwordsDontMatch')})
|
||||
return
|
||||
}
|
||||
|
||||
await this.passwordUpdateService.update(this.passwordUpdate)
|
||||
this.$message.success({message: this.$t('user.settings.passwordUpdateSuccess')})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
133
src/views/user/settings/TOTP.vue
Normal file
133
src/views/user/settings/TOTP.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<card :title="$t('user.settings.totp.title')" v-if="totpEnabled">
|
||||
<x-button
|
||||
:loading="totpService.loading"
|
||||
@click="totpEnroll()"
|
||||
v-if="!totpEnrolled && totp.secret === ''">
|
||||
{{ $t('user.settings.totp.enroll') }}
|
||||
</x-button>
|
||||
<template v-else-if="totp.secret !== '' && !totp.enabled">
|
||||
<p>
|
||||
{{ $t('user.settings.totp.finishSetupPart1') }}
|
||||
<strong>{{ totp.secret }}</strong><br/>
|
||||
{{ $t('user.settings.totp.finishSetupPart2') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('user.settings.totp.scanQR') }}<br/>
|
||||
<img :src="totpQR" alt=""/>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="totpConfirmPasscode">{{ $t('user.settings.totp.passcode') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
autocomplete="one-time-code"
|
||||
@keyup.enter="totpConfirm"
|
||||
class="input"
|
||||
id="totpConfirmPasscode"
|
||||
:placeholder="$t('user.settings.totp.passcodePlaceholder')"
|
||||
type="text"
|
||||
v-model="totpConfirmPasscode"/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="totpConfirm">{{ $t('misc.confirm') }}</x-button>
|
||||
</template>
|
||||
<template v-else-if="totp.secret !== '' && totp.enabled">
|
||||
<p>
|
||||
{{ $t('user.settings.totp.setupSuccess') }}
|
||||
</p>
|
||||
<p v-if="!totpDisableForm">
|
||||
<x-button @click="totpDisableForm = true" class="is-danger">{{ $t('misc.disable') }}</x-button>
|
||||
</p>
|
||||
<div v-if="totpDisableForm">
|
||||
<div class="field">
|
||||
<label class="label" for="currentPassword">{{ $t('user.settings.totp.enterPassword') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@keyup.enter="totpDisable"
|
||||
class="input"
|
||||
id="currentPassword"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-focus
|
||||
v-model="totpDisablePassword"/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="totpDisable" class="is-danger">
|
||||
{{ $t('user.settings.totp.disable') }}
|
||||
</x-button>
|
||||
<x-button @click="totpDisableForm = false" type="tertary" class="ml-2">
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</template>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TotpService from '@/services/totp'
|
||||
import TotpModel from '@/models/totp'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'user-settings-totp',
|
||||
data() {
|
||||
return {
|
||||
totpService: new TotpService(),
|
||||
totp: new TotpModel(),
|
||||
totpQR: '',
|
||||
totpEnrolled: false,
|
||||
totpConfirmPasscode: '',
|
||||
totpDisableForm: false,
|
||||
totpDisablePassword: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.totpStatus()
|
||||
},
|
||||
computed: mapState({
|
||||
totpEnabled: state => state.config.totpEnabled,
|
||||
}),
|
||||
mounted() {
|
||||
this.setTitle(`${this.$t('user.settings.totp.title')} - ${this.$t('user.settings.title')}`)
|
||||
},
|
||||
methods: {
|
||||
async totpStatus() {
|
||||
if (!this.totpEnabled) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.totp = await this.totpService.get()
|
||||
this.totpSetQrCode()
|
||||
} catch(e) {
|
||||
// Error code 1016 means totp is not enabled, we don't need an error in that case.
|
||||
if (e.response && e.response.data && e.response.data.code && e.response.data.code === 1016) {
|
||||
this.totpEnrolled = false
|
||||
return
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async totpSetQrCode() {
|
||||
const qr = await this.totpService.qrcode()
|
||||
this.totpQR = window.URL.createObjectURL(qr)
|
||||
},
|
||||
async totpEnroll() {
|
||||
this.totp = await this.totpService.enroll()
|
||||
this.totpEnrolled = true
|
||||
this.totpSetQrCode()
|
||||
},
|
||||
async totpConfirm() {
|
||||
await this.totpService.enable({passcode: this.totpConfirmPasscode})
|
||||
this.totp.enabled = true
|
||||
this.$message.success({message: this.$t('user.settings.totp.confirmSuccess')})
|
||||
},
|
||||
async totpDisable() {
|
||||
await this.totpService.disable({password: this.totpDisablePassword})
|
||||
this.totpEnrolled = false
|
||||
this.totp = new TotpModel()
|
||||
this.$message.success({message: this.$t('user.settings.totp.disableSuccess')})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
Loading…
Reference in a new issue