web/satellite: added enabling user MFA functionality to account settings
Added feature flagged functionality for enabling user MFA. Added new Popup where user will scan qr code and confirm enabling by entering passcode from MFA app. Also recovery codes will be visible afterwords Change-Id: Ie8d1bc83c941a08fd8701442601a2d20126c8892
This commit is contained in:
parent
0d8010e353
commit
e463eb17ac
3051
web/satellite/package-lock.json
generated
3051
web/satellite/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,9 @@
|
||||
"graphql": "15.3.0",
|
||||
"graphql-tag": "2.11.0",
|
||||
"load-script": "1.0.0",
|
||||
"otplib": "12.0.1",
|
||||
"pbkdf2": "3.1.1",
|
||||
"qrcode": "1.4.4",
|
||||
"stripe": "8.96.0",
|
||||
"vue": "2.6.12",
|
||||
"vue-class-component": "7.2.5",
|
||||
@ -39,6 +41,7 @@
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.11.0",
|
||||
"@types/node": "13.11.1",
|
||||
"@types/pbkdf2": "3.1.0",
|
||||
"@types/qrcode": "1.4.1",
|
||||
"@types/vue2-datepicker": "3.3.0",
|
||||
"@vue/cli-plugin-babel": "4.5.6",
|
||||
"@vue/cli-plugin-typescript": "4.5.6",
|
||||
|
@ -280,7 +280,7 @@ export class AuthHttpApi {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('Can not enable MFA. Please try again later');
|
||||
throw new Error('Can not disable MFA. Please try again later');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,6 +300,6 @@ export class AuthHttpApi {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('Can not enable MFA. Please try again later');
|
||||
throw new Error('Can not generate MFA recovery codes. Please try again later');
|
||||
}
|
||||
}
|
||||
|
@ -21,15 +21,6 @@
|
||||
:init-value="userInfo.fullName"
|
||||
@setData="setFullName"
|
||||
/>
|
||||
<HeaderedInput
|
||||
class="full-input"
|
||||
label="Nickname"
|
||||
placeholder="Enter Nickname"
|
||||
width="100%"
|
||||
ref="shortNameInput"
|
||||
:init-value="userInfo.shortName"
|
||||
@setData="setShortName"
|
||||
/>
|
||||
<div class="edit-profile-popup__form-container__button-container">
|
||||
<VButton
|
||||
label="Cancel"
|
||||
@ -83,10 +74,6 @@ export default class EditProfilePopup extends Vue {
|
||||
this.fullNameError = '';
|
||||
}
|
||||
|
||||
public setShortName(value: string): void {
|
||||
this.userInfo.setShortName(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates name and tries to update user info and close popup.
|
||||
*/
|
||||
@ -107,7 +94,7 @@ export default class EditProfilePopup extends Vue {
|
||||
|
||||
await this.$notify.success('Account info successfully updated!');
|
||||
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
|
||||
await this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,14 +2,14 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="profile-container">
|
||||
<h1 class="profile-container__title">Account Settings</h1>
|
||||
<div class="profile-container__edit-profile no-margin" >
|
||||
<div class="profile-container__edit-profile__row">
|
||||
<div class="profile-container__edit-profile__avatar">
|
||||
<h1 class="profile-container__edit-profile__avatar__letter">{{avatarLetter}}</h1>
|
||||
<div class="settings">
|
||||
<h1 class="settings__title">Account Settings</h1>
|
||||
<div class="settings__edit-profile" >
|
||||
<div class="settings__edit-profile__row">
|
||||
<div class="settings__edit-profile__avatar">
|
||||
<h1 class="settings__edit-profile__avatar__letter">{{avatarLetter}}</h1>
|
||||
</div>
|
||||
<div class="profile-container__edit-profile__text">
|
||||
<div class="settings__edit-profile__text">
|
||||
<h2 class="profile-bold-text">Edit Profile</h2>
|
||||
<h3 class="profile-regular-text">This information will be visible to all users</h3>
|
||||
</div>
|
||||
@ -19,11 +19,11 @@
|
||||
@click="toggleEditProfilePopup"
|
||||
/>
|
||||
</div>
|
||||
<div class="profile-container__secondary-container">
|
||||
<div class="profile-container__secondary-container__change-password">
|
||||
<div class="profile-container__edit-profile__row">
|
||||
<ChangePasswordIcon class="profile-container__secondary-container__img"/>
|
||||
<div class="profile-container__secondary-container__change-password__text-container">
|
||||
<div class="settings__secondary-container">
|
||||
<div class="settings__secondary-container__change-password">
|
||||
<div class="settings__edit-profile__row">
|
||||
<ChangePasswordIcon class="settings__secondary-container__img"/>
|
||||
<div class="settings__secondary-container__change-password__text-container">
|
||||
<h2 class="profile-bold-text">Change Password</h2>
|
||||
<h3 class="profile-regular-text">6 or more characters</h3>
|
||||
</div>
|
||||
@ -33,17 +33,31 @@
|
||||
@click="toggleChangePasswordPopup"
|
||||
/>
|
||||
</div>
|
||||
<div class="profile-container__secondary-container__email-container">
|
||||
<div class="profile-container__edit-profile__row">
|
||||
<EmailIcon class="profile-container__secondary-container__img"/>
|
||||
<div class="profile-container__secondary-container__email-container__text-container">
|
||||
<div class="settings__secondary-container__email-container">
|
||||
<div class="settings__edit-profile__row">
|
||||
<EmailIcon class="settings__secondary-container__img"/>
|
||||
<div class="settings__secondary-container__email-container__text-container">
|
||||
<h2 class="profile-bold-text email">{{user.email}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings__mfa" v-if="isMFAEnabled">
|
||||
<h2 class="profile-bold-text">Two-Factor Authentication</h2>
|
||||
<p class="profile-regular-text">
|
||||
To increase your account security, we strongly recommend enabling 2FA on your account.
|
||||
</p>
|
||||
<VButton
|
||||
class="settings__mfa__button"
|
||||
label="Enable 2FA"
|
||||
width="173px"
|
||||
height="44px"
|
||||
:on-press="toggleEnableMFAModal"
|
||||
/>
|
||||
</div>
|
||||
<ChangePasswordPopup v-if="isChangePasswordPopupShown"/>
|
||||
<EditProfilePopup v-if="isEditProfilePopupShown"/>
|
||||
<EnableMFAPopup v-if="isEnableMFAModal" :toggle-modal="toggleEnableMFAModal"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -53,6 +67,7 @@ import { Component, Vue } from 'vue-property-decorator';
|
||||
import ChangePasswordPopup from '@/components/account/ChangePasswordPopup.vue';
|
||||
import DeleteAccountPopup from '@/components/account/DeleteAccountPopup.vue';
|
||||
import EditProfilePopup from '@/components/account/EditProfilePopup.vue';
|
||||
import EnableMFAPopup from '@/components/account/mfa/EnableMFAPopup.vue';
|
||||
import VButton from '@/components/common/VButton.vue';
|
||||
|
||||
import ChangePasswordIcon from '@/../static/images/account/profile/changePassword.svg';
|
||||
@ -62,6 +77,7 @@ import EditIcon from '@/../static/images/common/edit.svg';
|
||||
import { USER_ACTIONS } from '@/store/modules/users';
|
||||
import { User } from '@/types/users';
|
||||
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@ -72,9 +88,13 @@ import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
||||
DeleteAccountPopup,
|
||||
ChangePasswordPopup,
|
||||
EditProfilePopup,
|
||||
EnableMFAPopup,
|
||||
},
|
||||
})
|
||||
export default class SettingsArea extends Vue {
|
||||
public isMFAEnabled: boolean = MetaUtils.getMetaContent('mfa-enabled') === 'true';
|
||||
public isEnableMFAModal = false;
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render where user info is fetching.
|
||||
*/
|
||||
@ -82,6 +102,13 @@ export default class SettingsArea extends Vue {
|
||||
this.$store.dispatch(USER_ACTIONS.GET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles enable MFA modal visibility.
|
||||
*/
|
||||
public toggleEnableMFAModal(): void {
|
||||
this.isEnableMFAModal = !this.isEnableMFAModal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens delete account popup.
|
||||
*/
|
||||
@ -124,13 +151,6 @@ export default class SettingsArea extends Vue {
|
||||
return this.$store.state.appStateModule.appState.isChangePasswordPopupShown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if delete account popup is shown.
|
||||
*/
|
||||
public get isDeleteAccountPopupShown(): boolean {
|
||||
return this.$store.state.appStateModule.appState.isDeleteAccountPopupShown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first letter of user name.
|
||||
*/
|
||||
@ -141,10 +161,10 @@ export default class SettingsArea extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-container {
|
||||
.settings {
|
||||
position: relative;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
padding-bottom: 100px;
|
||||
padding-bottom: 70px;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
@ -159,16 +179,13 @@ export default class SettingsArea extends Vue {
|
||||
width: calc(100% - 80px);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 37px 40px;
|
||||
margin-top: 40px;
|
||||
background-color: #fff;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
@ -194,7 +211,6 @@ export default class SettingsArea extends Vue {
|
||||
|
||||
&__secondary-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
@ -203,7 +219,6 @@ export default class SettingsArea extends Vue {
|
||||
height: 66px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 37px 40px;
|
||||
@ -219,7 +234,6 @@ export default class SettingsArea extends Vue {
|
||||
height: 66px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 37px 40px;
|
||||
@ -236,10 +250,17 @@ export default class SettingsArea extends Vue {
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin-top: 0;
|
||||
&__mfa {
|
||||
margin-top: 40px;
|
||||
padding: 40px;
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
|
||||
&__button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-svg {
|
||||
@ -265,24 +286,19 @@ export default class SettingsArea extends Vue {
|
||||
.profile-bold-text {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
color: #354049;
|
||||
margin-block-start: 0.5em;
|
||||
margin-block-end: 0.5em;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
word-break: break-all;
|
||||
max-height: 80px;
|
||||
}
|
||||
|
||||
.profile-regular-text {
|
||||
margin-block-start: 0.5em;
|
||||
margin-block-end: 0.5em;
|
||||
margin: 10px 0;
|
||||
color: #afb7c1;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.email {
|
||||
user-select: text;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
@ -308,6 +324,8 @@ export default class SettingsArea extends Vue {
|
||||
@media screen and (max-height: 825px) {
|
||||
|
||||
.profile-container {
|
||||
height: 535px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&__secondary-container {
|
||||
margin-top: 20px;
|
||||
@ -322,46 +340,4 @@ export default class SettingsArea extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 790px) {
|
||||
|
||||
.profile-container {
|
||||
height: 535px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar,
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 770px) {
|
||||
|
||||
.profile-container {
|
||||
height: 515px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 750px) {
|
||||
|
||||
.profile-container {
|
||||
height: 495px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 730px) {
|
||||
|
||||
.profile-container {
|
||||
height: 475px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 710px) {
|
||||
|
||||
.profile-container {
|
||||
height: 455px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
80
web/satellite/src/components/account/mfa/ConfirmMFAInput.vue
Normal file
80
web/satellite/src/components/account/mfa/ConfirmMFAInput.vue
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="confirm-mfa">
|
||||
<label for="confirm-mfa" class="confirm-mfa__label">
|
||||
<span class="confirm-mfa__label__info">2FA Code</span>
|
||||
<span class="confirm-mfa__label__error" v-if="isError">Invalid code. Please re-enter.</span>
|
||||
</label>
|
||||
<input
|
||||
id="confirm-mfa"
|
||||
class="confirm-mfa__input"
|
||||
placeholder="000000"
|
||||
type="number"
|
||||
@input="event => onInput(event.target.value)"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class ConfirmMFAInput extends Vue {
|
||||
@Prop({default: () => false})
|
||||
public readonly onInput: (value: string) => void;
|
||||
@Prop({default: false})
|
||||
public readonly isError: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.confirm-mfa {
|
||||
width: 100%;
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&__info {
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
color: #354049;
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
text-align: right;
|
||||
color: #ce3030;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: calc(100% - 40px);
|
||||
margin-top: 5px;
|
||||
background: #fff;
|
||||
border: 1px solid #a9b5c1;
|
||||
border-radius: 6px;
|
||||
padding: 15px 20px;
|
||||
font-size: 16px;
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
322
web/satellite/src/components/account/mfa/EnableMFAPopup.vue
Normal file
322
web/satellite/src/components/account/mfa/EnableMFAPopup.vue
Normal file
@ -0,0 +1,322 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="enable-mfa">
|
||||
<div class="enable-mfa__container">
|
||||
<h1 class="enable-mfa__container__title">Two-Factor Authentication</h1>
|
||||
<p class="enable-mfa__container__subtitle" v-if="isScan">
|
||||
Scan this QR code in your favorite TOTP app to get get started.
|
||||
</p>
|
||||
<p class="enable-mfa__container__subtitle max-width" v-if="isEnable">
|
||||
Enter the authentication code generated in your TOTP app to confirm your account is connected.
|
||||
</p>
|
||||
<p class="enable-mfa__container__subtitle" v-if="isCodes">
|
||||
Save recovery codes.
|
||||
</p>
|
||||
<div class="enable-mfa__container__scan" v-if="isScan">
|
||||
<h2 class="enable-mfa__container__scan__title">Scan this QR Code</h2>
|
||||
<p class="enable-mfa__container__scan__subtitle">Scan the following QR code in your OTP app.</p>
|
||||
<div class="enable-mfa__container__scan__qr">
|
||||
<canvas class="enable-mfa__container__scan__qr__canvas" ref="canvas"/>
|
||||
</div>
|
||||
<p class="enable-mfa__container__scan__subtitle">Unable to scan? Use the following code instead:</p>
|
||||
<p class="enable-mfa__container__scan__secret">{{secret}}</p>
|
||||
</div>
|
||||
<div class="enable-mfa__container__confirm" v-if="isEnable">
|
||||
<h2 class="enable-mfa__container__confirm__title">Confirm Authentication Code</h2>
|
||||
<ConfirmMFAInput :on-input="onConfirmInput" :is-error="isError"/>
|
||||
</div>
|
||||
<div class="enable-mfa__container__codes" v-if="isCodes">
|
||||
<h2 class="enable-mfa__container__codes__title max-width">
|
||||
Please save these codes somewhere to be able to recover access to your account.
|
||||
</h2>
|
||||
<p
|
||||
class="enable-mfa__container__codes__value"
|
||||
v-for="(code, index) in recoveryCodes"
|
||||
:key="index"
|
||||
>
|
||||
{{code}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="enable-mfa__container__buttons">
|
||||
<VButton
|
||||
class="cancel-button"
|
||||
label="Cancel"
|
||||
width="50%"
|
||||
height="44px"
|
||||
is-white="true"
|
||||
:on-press="toggleModal"
|
||||
/>
|
||||
<VButton
|
||||
v-if="isScan"
|
||||
label="Continue"
|
||||
width="50%"
|
||||
height="44px"
|
||||
:on-press="showEnable"
|
||||
/>
|
||||
<VButton
|
||||
v-if="isEnable"
|
||||
label="Enable"
|
||||
width="50%"
|
||||
height="44px"
|
||||
:on-press="enable"
|
||||
:is-disabled="!confirmPasscode || isLoading"
|
||||
/>
|
||||
<VButton
|
||||
v-if="isCodes"
|
||||
label="Done"
|
||||
width="50%"
|
||||
height="44px"
|
||||
:on-press="toggleModal"
|
||||
/>
|
||||
</div>
|
||||
<div class="enable-mfa__container__close-container" @click="toggleModal">
|
||||
<CloseCrossIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { authenticator } from 'otplib';
|
||||
import QRCode from 'qrcode';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import ConfirmMFAInput from '@/components/account/mfa/ConfirmMFAInput.vue';
|
||||
import VButton from '@/components/common/VButton.vue';
|
||||
|
||||
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ConfirmMFAInput,
|
||||
CloseCrossIcon,
|
||||
VButton,
|
||||
},
|
||||
})
|
||||
export default class EnableMFAPopup extends Vue {
|
||||
@Prop({default: () => false})
|
||||
public readonly toggleModal: () => void;
|
||||
|
||||
public readonly secret = authenticator.generateSecret();
|
||||
public readonly qrLink =
|
||||
`otpauth://totp/${encodeURIComponent(this.$store.getters.user.email)}?secret=${this.secret}&issuer=${encodeURIComponent('STORJ DCS')}&algorithm=SHA1&digits=6&period=30`;
|
||||
public isScan = true;
|
||||
public isEnable = false;
|
||||
public isCodes = false;
|
||||
public isError = false;
|
||||
public recoveryCodes: string[] = ['test', 'test', 'test', 'test', 'test', 'test', 'test', 'test', 'test', 'test'];
|
||||
|
||||
private confirmPasscode = '';
|
||||
private isLoading = false;
|
||||
|
||||
public $refs!: {
|
||||
canvas: HTMLCanvasElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mounted lifecycle hook after initial render.
|
||||
* Renders QR code.
|
||||
*/
|
||||
public async mounted(): Promise<void> {
|
||||
await QRCode.toCanvas(this.$refs.canvas, this.qrLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles view to Enable MFA state.
|
||||
*/
|
||||
public showEnable(): void {
|
||||
this.isScan = false;
|
||||
this.isEnable = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles view to MFA Recovery Codes state.
|
||||
*/
|
||||
public showCodes(): void {
|
||||
this.isEnable = false;
|
||||
this.isCodes = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets confirmation passcode value from input.
|
||||
*/
|
||||
public onConfirmInput(value: string): void {
|
||||
this.isError = false;
|
||||
this.confirmPasscode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables user MFA and sets view to Recovery Codes state.
|
||||
*/
|
||||
public async enable(): Promise<void> {
|
||||
if (!this.confirmPasscode || this.isLoading || this.isError) return;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// TODO: enable when backend is ready
|
||||
// await this.$store.dispatch(USER_ACTIONS.ENABLE_USER_MFA, {secret: this.secret, passcode: this.confirmPasscode})
|
||||
|
||||
await this.$notify.success('MFA was enabled successfully');
|
||||
|
||||
this.showCodes();
|
||||
} catch (error) {
|
||||
await this.$notify.error(error.message);
|
||||
this.isError = true;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.enable-mfa {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
background: rgba(27, 37, 51, 0.75);
|
||||
|
||||
&__container {
|
||||
padding: 60px;
|
||||
height: fit-content;
|
||||
margin-top: 100px;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 28px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
margin: 0 0 45px 0;
|
||||
}
|
||||
|
||||
&__scan {
|
||||
padding: 25px;
|
||||
background: #f5f6fa;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: calc(100% - 50px);
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 14px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&__qr {
|
||||
margin: 30px 0;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
|
||||
&__canvas {
|
||||
height: 200px !important;
|
||||
width: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__secret {
|
||||
margin: 5px 0 0 0;
|
||||
font-family: 'font_medium', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&__confirm,
|
||||
&__codes {
|
||||
padding: 25px;
|
||||
background: #f5f6fa;
|
||||
border-radius: 6px;
|
||||
width: calc(100% - 50px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&__close-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 30px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover .close-cross-svg-path {
|
||||
fill: #2683ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.max-width {
|
||||
max-width: 485px;
|
||||
}
|
||||
|
||||
@media screen and (max-height: 900px) {
|
||||
|
||||
.enable-mfa {
|
||||
padding-bottom: 20px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -90,10 +90,10 @@ export function makeUsersModule(api: UsersApi): StoreModule<UsersState> {
|
||||
|
||||
return user;
|
||||
},
|
||||
[ENABLE_USER_MFA]: async function (_): Promise<void> {
|
||||
[DISABLE_USER_MFA]: async function (_): Promise<void> {
|
||||
await api.disableUserMFA();
|
||||
},
|
||||
[DISABLE_USER_MFA]: async function (_, request: EnableUserMFARequest): Promise<void> {
|
||||
[ENABLE_USER_MFA]: async function (_, request: EnableUserMFARequest): Promise<void> {
|
||||
await api.enableUserMFA(request);
|
||||
},
|
||||
[GENERATE_USER_MFA_RECOVERY_CODES]: async function ({commit}: any): Promise<void> {
|
||||
|
Loading…
Reference in New Issue
Block a user