web/satellite/vuetify-poc: require MFA code to generate MFA recovery codes
This change uses the code protected MFA code generation endpoint. It requires a code from the user before generating new recovery codes. Issue: https://github.com/storj/storj-private/issues/433 Change-Id: I38c7c6f543a1d0c68aa1c2e9092e76fed2448467
This commit is contained in:
parent
5bbd477a58
commit
0a063fdeb3
@ -26,21 +26,54 @@
|
||||
</template>
|
||||
</v-card-item>
|
||||
<v-divider class="mx-8" />
|
||||
<v-card-item class="px-8 py-4">
|
||||
<p>Please save these codes somewhere to be able to recover access to your account.</p>
|
||||
</v-card-item>
|
||||
<v-divider class="mx-8" />
|
||||
<v-card-item class="px-8 py-4">
|
||||
<p
|
||||
v-for="(code, index) in userMFARecoveryCodes"
|
||||
:key="index"
|
||||
>
|
||||
{{ code }}
|
||||
</p>
|
||||
</v-card-item>
|
||||
<v-divider class="mx-8 mb-4" />
|
||||
|
||||
<template v-if="isConfirmCode">
|
||||
<v-card-item class="px-8 py-4">
|
||||
<p>Enter the authentication code generated in your two-factor application to regenerate recovery codes.</p>
|
||||
</v-card-item>
|
||||
<v-divider class="mx-8" />
|
||||
<v-card-item class="px-8 pt-4 pb-0">
|
||||
<v-form v-model="formValid" class="pt-2" @submit.prevent="regenerate">
|
||||
<v-text-field
|
||||
v-model="confirmPasscode"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:hint="useRecoveryCode ? '' : 'e.g.: 000000'"
|
||||
:rules="rules"
|
||||
:error-messages="isError ? 'Invalid code. Please re-enter.' : ''"
|
||||
:label="useRecoveryCode ? 'Recovery code' : '2FA Code'"
|
||||
:hide-details="false"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-item>
|
||||
<v-card-item class="px-8 py-0">
|
||||
<a class="text-decoration-underline" style="cursor: pointer;" @click="toggleRecoveryCodeState">
|
||||
{{ useRecoveryCode ? "or use 2FA code" : "or use a recovery code" }}
|
||||
</a>
|
||||
</v-card-item>
|
||||
|
||||
<v-divider class="mx-8 my-4" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-card-item class="px-8 py-4">
|
||||
<p>Please save these codes somewhere to be able to recover access to your account.</p>
|
||||
</v-card-item>
|
||||
<v-divider class="mx-8" />
|
||||
<v-card-item class="px-8 py-4">
|
||||
<p
|
||||
v-for="(code, index) in userMFARecoveryCodes"
|
||||
:key="index"
|
||||
>
|
||||
{{ code }}
|
||||
</p>
|
||||
</v-card-item>
|
||||
<v-divider class="mx-8 mb-4" />
|
||||
</template>
|
||||
|
||||
<v-card-actions dense class="px-7 pb-5 pt-0">
|
||||
<v-col class="px-0">
|
||||
<v-col v-if="!isConfirmCode" class="px-0">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@ -50,13 +83,35 @@
|
||||
Done
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col v-if="isConfirmCode" class="pl-0">
|
||||
<v-btn
|
||||
:disabled="isLoading"
|
||||
color="default"
|
||||
variant="outlined"
|
||||
block
|
||||
@click="model = false"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col v-if="isConfirmCode" class="pr-0">
|
||||
<v-btn
|
||||
:loading="isLoading"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
block
|
||||
@click="regenerate"
|
||||
>
|
||||
Regenerate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
VBtn,
|
||||
VCard,
|
||||
@ -66,15 +121,21 @@ import {
|
||||
VCol,
|
||||
VDialog,
|
||||
VDivider,
|
||||
VRow,
|
||||
VForm,
|
||||
VTextField,
|
||||
} from 'vuetify/components';
|
||||
|
||||
import { AuthHttpApi } from '@/api/auth';
|
||||
import { useUsersStore } from '@/store/modules/usersStore';
|
||||
import { useLoading } from '@/composables/useLoading';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
|
||||
const auth: AuthHttpApi = new AuthHttpApi();
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const notify = useNotify();
|
||||
const { withLoading, isLoading } = useLoading();
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean,
|
||||
@ -84,15 +145,79 @@ const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void,
|
||||
}>();
|
||||
|
||||
const confirmPasscode = ref<string>('');
|
||||
const isError = ref<boolean>(false);
|
||||
const formValid = ref<boolean>(false);
|
||||
const isConfirmCode = ref(true);
|
||||
const useRecoveryCode = ref<boolean>(false);
|
||||
|
||||
const model = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const rules = computed(() => {
|
||||
if (useRecoveryCode.value) {
|
||||
return [
|
||||
(value: string) => (!!value || 'Can\'t be empty'),
|
||||
];
|
||||
}
|
||||
return [
|
||||
(value: string) => (!!value || 'Can\'t be empty'),
|
||||
(value: string) => (!value.includes(' ') || 'Can\'t contain spaces'),
|
||||
(value: string) => (!!parseInt(value) || 'Can only be numbers'),
|
||||
(value: string) => (value.length === 6 || 'Can only be 6 numbers long'),
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns user MFA recovery codes from store.
|
||||
*/
|
||||
const userMFARecoveryCodes = computed((): string[] => {
|
||||
return usersStore.state.userMFARecoveryCodes;
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles whether the MFA recovery code input is shown.
|
||||
*/
|
||||
function toggleRecoveryCodeState(): void {
|
||||
isError.value = false;
|
||||
confirmPasscode.value = '';
|
||||
useRecoveryCode.value = !useRecoveryCode.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates user MFA codes and sets view to Recovery Codes state.
|
||||
*/
|
||||
function regenerate(): void {
|
||||
if (!confirmPasscode.value || isLoading.value || isError.value) return;
|
||||
|
||||
withLoading(async () => {
|
||||
try {
|
||||
const code = useRecoveryCode.value ? { recoveryCode: confirmPasscode.value } : { passcode: confirmPasscode.value };
|
||||
await usersStore.regenerateUserMFARecoveryCodes(code);
|
||||
isConfirmCode.value = false;
|
||||
confirmPasscode.value = '';
|
||||
|
||||
notify.success('MFA codes were regenerated successfully');
|
||||
} catch (error) {
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.MFA_CODES_MODAL);
|
||||
isError.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(confirmPasscode, () => {
|
||||
isError.value = false;
|
||||
});
|
||||
|
||||
watch(model, shown => {
|
||||
if (shown) {
|
||||
return;
|
||||
}
|
||||
isConfirmCode.value = true;
|
||||
useRecoveryCode.value = false;
|
||||
confirmPasscode.value = '';
|
||||
isError.value = false;
|
||||
});
|
||||
</script>
|
@ -205,7 +205,6 @@ async function toggleEnableMFADialog() {
|
||||
|
||||
async function toggleRecoveryCodesDialog() {
|
||||
try {
|
||||
await usersStore.generateUserMFARecoveryCodes();
|
||||
isRecoveryCodesDialogShown.value = true;
|
||||
} catch (error) {
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.ACCOUNT_SETTINGS_AREA);
|
||||
|
Loading…
Reference in New Issue
Block a user