mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-24 06:48:35 +02:00
Allow to enable only webauthn
This commit is contained in:
@@ -24,5 +24,5 @@ export default {
|
||||
},
|
||||
deleteFido(id) {
|
||||
return repository.delete(`/fido/${id}/`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card flat>
|
||||
<v-card-title>
|
||||
<span class="text-h6">
|
||||
{{ $gettext('Backup codes') }}
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<template v-if="tfa_enabled">
|
||||
<v-alert type="info" class="mb-2">
|
||||
{{
|
||||
$gettext(
|
||||
"You already have generated backup codes for this account. If you don't remember them or have used them all, you generate new ones."
|
||||
)
|
||||
}}
|
||||
</v-alert>
|
||||
<v-form ref="editTFAForm">
|
||||
<label class="m-label">{{ $gettext('Password') }} </label>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
autocomplete="new-password"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
:rules="[rules.required]"
|
||||
:error-messages="passwordError"
|
||||
/>
|
||||
<div class="mt-5">
|
||||
<v-btn
|
||||
:loading="loadingReset"
|
||||
color="success"
|
||||
@click="resetRecoveryCodes"
|
||||
>
|
||||
{{ $gettext('Reset recovery codes') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div tag="p" class="my-4">
|
||||
{{
|
||||
$gettext(
|
||||
'No two-factor authentificator is yet set for your account.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
<v-dialog v-model="showCodesResetDialog" max-width="800px" persistent>
|
||||
<RecoveryCodesResetDialog
|
||||
:tokens="newTokens"
|
||||
@close="closeRecoveryCodesResetDialog"
|
||||
/>
|
||||
</v-dialog>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import accountApi from '@/api/account'
|
||||
import RecoveryCodesResetDialog from './RecoveryCodesResetDialog'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore, useAuthStore } from '@/stores'
|
||||
import rules from '@/plugins/rules'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const busStore = useBusStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const tfa_enabled = computed(() => authStore.authUser.tfa_enabled)
|
||||
|
||||
const loadingReset = ref(false)
|
||||
const editTFAForm = ref()
|
||||
const password = ref('')
|
||||
const newTokens = ref([])
|
||||
const passwordError = ref()
|
||||
const showCodesResetDialog = ref(false)
|
||||
|
||||
async function resetRecoveryCodes() {
|
||||
const isValid = await editTFAForm.value.validate()
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
password: password.value,
|
||||
}
|
||||
loadingReset.value = true
|
||||
|
||||
accountApi
|
||||
.resetRecoveryCodes(payload)
|
||||
.then((response) => {
|
||||
newTokens.value = response.data.tokens
|
||||
showCodesResetDialog.value = true
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 400) {
|
||||
passwordError.value = error.response.data.password
|
||||
} else {
|
||||
busStore.displayNotification({
|
||||
msg: $gettext(error.response.data),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingReset.value = false
|
||||
editTFAForm.value.reset()
|
||||
})
|
||||
}
|
||||
|
||||
function closeRecoveryCodesResetDialog() {
|
||||
newTokens.value = []
|
||||
showCodesResetDialog.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -16,33 +16,8 @@
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<template v-if="browserCapable">
|
||||
<v-form ref="fidoForm" @submit.prevent="startFidoRegistration">
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
:label="$gettext('New device name')"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-btn
|
||||
color="success"
|
||||
type="submit"
|
||||
:loading="registrationLoading"
|
||||
>
|
||||
{{ $gettext('Setup device') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-alert type="error">
|
||||
{{
|
||||
$gettext(
|
||||
'Your browser does not seem compatible with WebAuthN'
|
||||
)
|
||||
}}
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-col>
|
||||
<v-col v-if="fidoCreds.length" cols="8">
|
||||
<v-col v-else cols="8">
|
||||
<h3>{{ $gettext('Registered Webauthn devices') }}</h3>
|
||||
<v-data-table-virtual
|
||||
:headers="headers"
|
||||
@@ -74,6 +49,38 @@
|
||||
</template>
|
||||
</v-data-table-virtual>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<template v-if="browserCapable">
|
||||
<h3>
|
||||
{{
|
||||
$gettext('Enter a name to register a new webauthn device.')
|
||||
}}
|
||||
</h3>
|
||||
<v-form ref="fidoForm" @submit.prevent="startFidoRegistration">
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
:label="$gettext('New device name')"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-btn
|
||||
color="success"
|
||||
type="submit"
|
||||
:loading="registrationLoading"
|
||||
>
|
||||
{{ $gettext('Setup device') }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-alert type="error">
|
||||
{{
|
||||
$gettext(
|
||||
'Your browser does not seem compatible with WebAuthN'
|
||||
)
|
||||
}}
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -95,7 +102,7 @@
|
||||
</ConfirmDialog>
|
||||
<v-dialog v-model="showBackupTokenDialog" max-width="800px" persistent>
|
||||
<RecoveryCodesResetDialog
|
||||
:tokens="newTokens"
|
||||
:tokens="tokens"
|
||||
@close="closeRecoveryCodesResetDialog"
|
||||
/>
|
||||
</v-dialog>
|
||||
@@ -153,16 +160,21 @@ async function startFidoRegistration() {
|
||||
const creationOption = await authApi.beginFidoRegistration()
|
||||
if (creationOption) {
|
||||
const options = parseCreationOptionsFromJSON(creationOption.data)
|
||||
const device = await create(options)
|
||||
const result = device.toJSON()
|
||||
result.name = name.value
|
||||
authStore.addFidoCred(result).then((res) => {
|
||||
if (res.status === 200) {
|
||||
tokens.value = res.data.tokens
|
||||
showBackupTokenDialog.value = true
|
||||
}
|
||||
})
|
||||
fidoForm.value.reset()
|
||||
try {
|
||||
const device = await create(options)
|
||||
const result = device.toJSON()
|
||||
result.name = name.value
|
||||
authStore.addFidoCred(result).then((res) => {
|
||||
if (res.status === 200) {
|
||||
tokens.value = res.data.tokens
|
||||
showBackupTokenDialog.value = true
|
||||
}
|
||||
})
|
||||
fidoForm.value.reset()
|
||||
} catch {
|
||||
registrationLoading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
registrationLoading.value = false
|
||||
}
|
||||
|
||||
+24
-72
@@ -47,7 +47,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-else-if="tokens.length">
|
||||
<template v-else-if="successSetup">
|
||||
<v-alert type="success">
|
||||
{{
|
||||
$gettext(
|
||||
@@ -55,22 +55,24 @@
|
||||
)
|
||||
}}
|
||||
</v-alert>
|
||||
<p class="mt-4">
|
||||
{{
|
||||
$gettext(
|
||||
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<v-table density="compact">
|
||||
<tbody>
|
||||
<tr v-for="token in tokens" :key="token">
|
||||
<td>{{ token }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<template v-if="tokens.length">
|
||||
<p class="mt-4">
|
||||
{{
|
||||
$gettext(
|
||||
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<v-table density="compact">
|
||||
<tbody>
|
||||
<tr v-for="token in tokens" :key="token">
|
||||
<td>{{ token }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="tfa_enabled">
|
||||
<template v-else-if="totp_enabled">
|
||||
<v-alert type="info" class="mb-2">
|
||||
{{
|
||||
$gettext(
|
||||
@@ -97,9 +99,6 @@
|
||||
>
|
||||
{{ $gettext('Disable 2FA') }}
|
||||
</v-btn>
|
||||
<v-btn :loading="loadingReset" @click="resetRecoveryCodes">
|
||||
{{ $gettext('Reset recovery codes') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
</template>
|
||||
@@ -117,18 +116,11 @@
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-dialog v-model="showCodesResetDialog" max-width="800px" persistent>
|
||||
<RecoveryCodesResetDialog
|
||||
:tokens="newTokens"
|
||||
@close="closeRecoveryCodesResetDialog"
|
||||
/>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import accountApi from '@/api/account'
|
||||
import RecoveryCodesResetDialog from './RecoveryCodesResetDialog'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { useBusStore, useAuthStore } from '@/stores'
|
||||
import { ref, computed } from 'vue'
|
||||
@@ -140,21 +132,18 @@ const busStore = useBusStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const editTFAForm = ref()
|
||||
|
||||
const newTokens = ref([])
|
||||
const pinCode = ref('')
|
||||
const pinCodeErrors = ref([])
|
||||
const key = ref(null)
|
||||
const qrURL = ref(null)
|
||||
const clicked = ref(false)
|
||||
const showCodesResetDialog = ref(false)
|
||||
const loadingDisable = ref(false)
|
||||
const loadingReset = ref(false)
|
||||
const password = ref('')
|
||||
const passwordError = ref([])
|
||||
const tokens = ref([])
|
||||
const successSetup = ref(false)
|
||||
|
||||
const tfa_enabled = computed(() => authStore.authUser.tfa_enabled)
|
||||
const totp_enabled = computed(() => authStore.authUser.totp_enabled)
|
||||
|
||||
function getKey() {
|
||||
accountApi.getKeyForTFASetup().then((resp) => {
|
||||
@@ -180,7 +169,10 @@ function finalizeTFASetup() {
|
||||
.then((response) => {
|
||||
key.value = null
|
||||
qrURL.value = null
|
||||
tokens.value = response.data.tokens
|
||||
if (response.status === 200) {
|
||||
tokens.value = response.data.tokens
|
||||
}
|
||||
successSetup.value = true
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 400) {
|
||||
@@ -218,46 +210,6 @@ async function disableTFA() {
|
||||
editTFAForm.value.reset()
|
||||
})
|
||||
}
|
||||
|
||||
async function resetRecoveryCodes() {
|
||||
const isValid = await editTFAForm.value.validate()
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
password: password.value,
|
||||
}
|
||||
loadingReset.value = true
|
||||
|
||||
accountApi
|
||||
.resetRecoveryCodes(payload)
|
||||
.then((response) => {
|
||||
newTokens.value = response.data.tokens
|
||||
showCodesResetDialog.value = true
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response.status === 400) {
|
||||
passwordError.value = error.response.data.password
|
||||
} else {
|
||||
busStore.displayNotification({
|
||||
msg: $gettext(error.response.data),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loadingReset.value = false
|
||||
editTFAForm.value.reset()
|
||||
})
|
||||
}
|
||||
|
||||
function closeRecoveryCodesResetDialog() {
|
||||
newTokens.value = []
|
||||
showCodesResetDialog.value = false
|
||||
}
|
||||
if (!tfa_enabled.value) {
|
||||
getKey()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -36,10 +36,10 @@ const { $gettext } = useGettext()
|
||||
|
||||
const props = defineProps({
|
||||
// the callback should return true or false, returning false inhibits the close event
|
||||
callback_agree : {
|
||||
callback_agree: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const dialog = ref(false)
|
||||
@@ -70,7 +70,7 @@ defineExpose({
|
||||
})
|
||||
|
||||
async function agree() {
|
||||
if (props.callback_agree !== null && ! (await props.callback_agree())) {
|
||||
if (props.callback_agree !== null && !(await props.callback_agree())) {
|
||||
return
|
||||
}
|
||||
storedResolve(true)
|
||||
|
||||
@@ -46,18 +46,28 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
async function getFidoCreds() {
|
||||
return authApi.getAllFidoRegistred().then((resp) => {
|
||||
fidoCreds.value = resp.data
|
||||
if (fidoCreds.value.length > 0) {
|
||||
authUser.value.tfa_enabled = true
|
||||
authUser.value.webauthn_enabled = true
|
||||
}
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
async function addFidoCred(result) {
|
||||
return authApi.endFidoRegistration(result).then(() => {
|
||||
return authApi.endFidoRegistration(result).then((res) => {
|
||||
getFidoCreds()
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteFidoCreds(id) {
|
||||
return authApi.deleteFido(id).then(() => {
|
||||
return authApi.deleteFido(id).then((res) => {
|
||||
fidoCreds.value = fidoCreds.value.filter((cred) => cred.id !== id)
|
||||
authUser.value.tfa_enabled = res.data.tfa_enabled
|
||||
if (!res.data.tfa_enabled) {
|
||||
authUser.value.webauthn_enabled = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
<ProfileForm />
|
||||
</v-window-item>
|
||||
<v-window-item value="security">
|
||||
<TwoFactorAuthForm />
|
||||
<TotpAuthForm />
|
||||
<BackupCodeAuthForm />
|
||||
<FidoAuthForm />
|
||||
</v-window-item>
|
||||
<v-window-item v-if="authStore.userHasMailbox" value="forward">
|
||||
@@ -47,7 +48,8 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ProfileForm from '@/components/account/ProfileForm.vue'
|
||||
import TwoFactorAuthForm from '@/components/account/TwoFactorAuthForm.vue'
|
||||
import TotpAuthForm from '@/components/account/TotpAuthForm.vue'
|
||||
import BackupCodeAuthForm from '@/components/account/BackupCodeAuthForm.vue'
|
||||
import ForwardForm from '@/components/account/ForwardForm.vue'
|
||||
import AutoReplyForm from '@/components/account/AutoReplyForm.vue'
|
||||
import APISetupForm from '@/components/account/APISetupForm.vue'
|
||||
|
||||
Reference in New Issue
Block a user