web/satellite: refactored modals to use common VModal component

Refactored MFA, project members, profile and change password modals to use common VModal component.
Didn't touch access grant modals.

Change-Id: I9af5277496a99ec907fbee57a84127064ca9c02b
This commit is contained in:
Vitalii 2022-08-03 16:29:58 +03:00 committed by Storj Robot
parent 1be5277c2a
commit 27f6fbdeda
20 changed files with 831 additions and 1137 deletions

View File

@ -16,7 +16,7 @@
</div>
<EditIcon
class="edit-svg"
@click="toggleEditProfilePopup"
@click="toggleEditProfileModal"
/>
</div>
<div class="settings__secondary-container">
@ -30,7 +30,7 @@
</div>
<EditIcon
class="edit-svg"
@click="toggleChangePasswordPopup"
@click="toggleChangePasswordModal"
/>
</div>
<div class="settings__secondary-container__email-container">
@ -65,7 +65,7 @@
label="Disable 2FA"
width="173px"
height="44px"
:on-press="toggleDisableMFAPopup"
:on-press="toggleDisableMFAModal"
is-deletion="true"
/>
<VButton
@ -79,32 +79,22 @@
</div>
</div>
</div>
<ChangePasswordPopup v-if="isChangePasswordPopupShown" />
<EditProfilePopup v-if="isEditProfilePopupShown" />
<EnableMFAPopup v-if="isEnableMFAPopup" :toggle-modal="toggleEnableMFAPopup" />
<DisableMFAPopup v-if="isDisableMFAPopup" :toggle-modal="toggleDisableMFAPopup" />
<MFARecoveryCodesPopup v-if="isMFACodesPopup" :toggle-modal="toggleMFACodesPopup" />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ChangePasswordPopup from '@/components/account/ChangePasswordPopup.vue';
import EditProfilePopup from '@/components/account/EditProfilePopup.vue';
import DisableMFAPopup from '@/components/account/mfa/DisableMFAPopup.vue';
import EnableMFAPopup from '@/components/account/mfa/EnableMFAPopup.vue';
import MFARecoveryCodesPopup from '@/components/account/mfa/MFARecoveryCodesPopup.vue';
import { USER_ACTIONS } from '@/store/modules/users';
import { User } from '@/types/users';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import VButton from '@/components/common/VButton.vue';
import ChangePasswordIcon from '@/../static/images/account/profile/changePassword.svg';
import EmailIcon from '@/../static/images/account/profile/email.svg';
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';
// @vue/component
@Component({
components: {
@ -112,18 +102,10 @@ import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
ChangePasswordIcon,
EmailIcon,
VButton,
ChangePasswordPopup,
EditProfilePopup,
EnableMFAPopup,
DisableMFAPopup,
MFARecoveryCodesPopup,
},
})
export default class SettingsArea extends Vue {
public isLoading = false;
public isEnableMFAPopup = false;
public isDisableMFAPopup = false;
public isMFACodesPopup = false;
/**
* Lifecycle hook after initial render where user info is fetching.
@ -142,7 +124,7 @@ export default class SettingsArea extends Vue {
try {
await this.$store.dispatch(USER_ACTIONS.GENERATE_USER_MFA_SECRET);
this.toggleEnableMFAPopup();
this.toggleEnableMFAModal();
} catch (error) {
await this.$notify.error(error.message);
}
@ -160,7 +142,7 @@ export default class SettingsArea extends Vue {
try {
await this.$store.dispatch(USER_ACTIONS.GENERATE_USER_MFA_RECOVERY_CODES);
this.toggleMFACodesPopup();
this.toggleMFACodesModal();
} catch (error) {
await this.$notify.error(error.message);
}
@ -169,38 +151,38 @@ export default class SettingsArea extends Vue {
}
/**
* Toggles enable MFA popup visibility.
* Toggles enable MFA modal visibility.
*/
public toggleEnableMFAPopup(): void {
this.isEnableMFAPopup = !this.isEnableMFAPopup;
public toggleEnableMFAModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ENABLE_MFA_MODAL_SHOWN);
}
/**
* Toggles disable MFA popup visibility.
* Toggles disable MFA modal visibility.
*/
public toggleDisableMFAPopup(): void {
this.isDisableMFAPopup = !this.isDisableMFAPopup;
public toggleDisableMFAModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_DISABLE_MFA_MODAL_SHOWN);
}
/**
* Toggles MFA recovery codes popup visibility.
* Toggles MFA recovery codes modal visibility.
*/
public toggleMFACodesPopup(): void {
this.isMFACodesPopup = !this.isMFACodesPopup;
public toggleMFACodesModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_MFA_RECOVERY_MODAL_SHOWN);
}
/**
* Opens change password popup.
*/
public toggleChangePasswordPopup(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
public toggleChangePasswordModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_MODAL_SHOWN);
}
/**
* Opens edit account info popup.
* Opens edit account info modal.
*/
public toggleEditProfilePopup(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
public toggleEditProfileModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_EDIT_PROFILE_MODAL_SHOWN);
}
/**
@ -210,20 +192,6 @@ export default class SettingsArea extends Vue {
return this.$store.getters.user;
}
/**
* Indicates if edit user info popup is shown.
*/
public get isEditProfilePopupShown(): boolean {
return this.$store.state.appStateModule.appState.isEditProfilePopupShown;
}
/**
* Indicates if change password popup is shown.
*/
public get isChangePasswordPopupShown(): boolean {
return this.$store.state.appStateModule.appState.isChangePasswordPopupShown;
}
/**
* Returns first letter of user name.
*/

View File

@ -13,6 +13,7 @@
class="confirm-mfa__input"
:placeholder="isRecovery ? 'Code' : '000000'"
:type="isRecovery ? 'text' : 'number'"
autofocus
@input="event => onInput(event.target.value)"
>
</div>

View File

@ -1,347 +0,0 @@
// 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 v-if="isScan" class="enable-mfa__container__subtitle">
Scan this QR code in your favorite TOTP app to get started.
</p>
<p v-if="isEnable" class="enable-mfa__container__subtitle max-width">
Enter the authentication code generated in your TOTP app to confirm your account is connected.
</p>
<p v-if="isCodes" class="enable-mfa__container__subtitle">
Save recovery codes.
</p>
<div v-if="isScan" class="enable-mfa__container__scan">
<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 ref="canvas" class="enable-mfa__container__scan__qr__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">{{ userMFASecret }}</p>
</div>
<div v-if="isEnable" class="enable-mfa__container__confirm">
<h2 class="enable-mfa__container__confirm__title">Confirm Authentication Code</h2>
<ConfirmMFAInput :on-input="onConfirmInput" :is-error="isError" />
</div>
<div v-if="isCodes" class="enable-mfa__container__codes">
<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
v-for="(code, index) in userMFARecoveryCodes"
: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 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';
import { USER_ACTIONS } from '@/store/modules/users';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
// @vue/component
@Component({
components: {
ConfirmMFAInput,
CloseCrossIcon,
VButton,
},
})
export default class EnableMFAPopup extends Vue {
@Prop({default: () => () => false})
public readonly toggleModal: () => void;
public readonly qrLink =
`otpauth://totp/${encodeURIComponent(this.$store.getters.user.email)}?secret=${this.userMFASecret}&issuer=${encodeURIComponent(`STORJ ${this.satellite}`)}&algorithm=SHA1&digits=6&period=30`;
public isScan = true;
public isEnable = false;
public isCodes = false;
public isError = false;
public isLoading = false;
public confirmPasscode = '';
public $refs!: {
canvas: HTMLCanvasElement;
};
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
/**
* 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 async showCodes(): Promise<void> {
await this.$store.dispatch(USER_ACTIONS.GENERATE_USER_MFA_RECOVERY_CODES);
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 {
await this.$store.dispatch(USER_ACTIONS.ENABLE_USER_MFA, this.confirmPasscode);
await this.$store.dispatch(USER_ACTIONS.GET);
await this.showCodes();
this.analytics.eventTriggered(AnalyticsEvent.MFA_ENABLED);
await this.$notify.success('MFA was enabled successfully');
} catch (error) {
await this.$notify.error(error.message);
this.isError = true;
}
this.isLoading = false;
}
/**
* Returns satellite name from store.
*/
private get satellite(): string {
return this.$store.state.appStateModule.satelliteName;
}
/**
* Returns pre-generated MFA secret from store.
*/
private get userMFASecret(): string {
return this.$store.state.usersModule.userMFASecret;
}
/**
* Returns user MFA recovery codes from store.
*/
private get userMFARecoveryCodes(): string[] {
return this.$store.state.usersModule.userMFARecoveryCodes;
}
}
</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: rgb(27 37 51 / 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;
}
&__subtitle {
font-size: 16px;
line-height: 21px;
text-align: center;
color: #000;
margin: 0 0 45px;
}
&__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;
}
&__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;
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>

View File

@ -1,141 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="mfa-codes">
<div class="mfa-codes__container">
<h1 class="mfa-codes__container__title">Two-Factor Authentication</h1>
<div class="mfa-codes__container__codes">
<p class="mfa-codes__container__codes__subtitle">
Please save these codes somewhere to be able to recover access to your account.
</p>
<p
v-for="(code, index) in userMFARecoveryCodes"
:key="index"
>
{{ code }}
</p>
</div>
<VButton
class="done-button"
label="Done"
width="100%"
height="44px"
:on-press="toggleModal"
/>
<div class="mfa-codes__container__close-container" @click="toggleModal">
<CloseCrossIcon />
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import VButton from '@/components/common/VButton.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
// @vue/component
@Component({
components: {
CloseCrossIcon,
VButton,
},
})
export default class MFARecoveryCodesPopup extends Vue {
@Prop({default: () => () => false})
public readonly toggleModal: () => void;
/**
* Returns MFA recovery codes from store.
*/
public get userMFARecoveryCodes(): string[] {
return this.$store.state.usersModule.userMFARecoveryCodes;
}
}
</script>
<style scoped lang="scss">
.mfa-codes {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
display: flex;
justify-content: center;
z-index: 1000;
background: rgb(27 37 51 / 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;
}
&__codes {
padding: 25px;
background: #f5f6fa;
border-radius: 6px;
width: calc(100% - 50px);
display: flex;
flex-direction: column;
align-items: center;
&__subtitle {
font-size: 16px;
line-height: 21px;
text-align: center;
color: #000;
margin: 0 0 30px;
max-width: 485px;
}
}
&__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;
}
}
}
}
.done-button {
margin-top: 30px;
}
@media screen and (max-height: 750px) {
.mfa-codes {
padding-bottom: 20px;
overflow-y: scroll;
}
}
</style>

View File

@ -1,112 +1,112 @@
// Copyright (C) 2019 Storj Labs, Inc.
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="add-user-container" @keyup.enter="onAddUsersClick" @keyup.esc="onClose">
<div id="addTeamMemberPopup" class="add-user">
<div class="add-user__main">
<div class="add-user__info-panel-container">
<h2 class="add-user__info-panel-container__main-label-text">Add Team Member</h2>
<img src="@/../static/images/team/addMember.jpg" alt="add team member image">
</div>
<div class="add-user__form-container">
<p v-if="!formError" class="add-user__form-container__common-label">Email Address</p>
<div v-if="formError" class="add-user__form-container__label">
<ErrorIcon alt="Red error icon" />
<p class="add-user__form-container__label__error">{{ formError }}</p>
<VModal :on-close="closeModal">
<template #content>
<div class="add-user">
<div class="add-user__main">
<div class="add-user__info-panel-container">
<h2 class="add-user__info-panel-container__main-label-text">Add Team Member</h2>
<img src="@/../static/images/team/addMember.jpg" alt="add team member image">
</div>
<div class="add-user__form-container__inputs-group" :class="{ 'scrollable': isInputsGroupScrollable }">
<div
v-for="(input, index) in inputs"
:key="index"
class="add-user__form-container__inputs-group__item"
>
<input
v-model="input.value"
placeholder="email@example.com"
class="no-error-input"
:class="{ 'error-input': input.error }"
@keyup="resetFormErrors(index)"
<div class="add-user__form-container">
<p v-if="!formError" class="add-user__form-container__common-label">Email Address</p>
<div v-if="formError" class="add-user__form-container__label">
<ErrorIcon alt="Red error icon" />
<p class="add-user__form-container__label__error">{{ formError }}</p>
</div>
<div class="add-user__form-container__inputs-group" :class="{ 'scrollable': isInputsGroupScrollable }">
<div
v-for="(input, index) in inputs"
:key="index"
class="add-user__form-container__inputs-group__item"
>
<DeleteFieldIcon
class="add-user__form-container__inputs-group__item__image"
@click="deleteInput(index)"
<input
v-model="input.value"
placeholder="email@example.com"
class="no-error-input"
:class="{ 'error-input': input.error }"
@keyup="resetFormErrors(index)"
>
<DeleteFieldIcon
class="add-user__form-container__inputs-group__item__image"
@click="deleteInput(index)"
/>
</div>
</div>
<div class="add-user-row">
<div id="add-user-button" class="add-user-row__item" @click="addInput">
<div :class="{ 'inactive-image': isMaxInputsCount }">
<AddFieldIcon class="add-user-row__item__image" />
</div>
<p class="add-user-row__item__label" :class="{ 'inactive-label': isMaxInputsCount }">Add More</p>
</div>
</div>
<div class="add-user__form-container__button-container">
<VButton
label="Cancel"
width="205px"
height="48px"
:on-press="closeModal"
is-transparent="true"
/>
<VButton
label="Add Team Members"
width="205px"
height="48px"
:on-press="onAddUsersClick"
:is-disabled="!isButtonActive"
/>
</div>
</div>
<div class="add-user-row">
<div id="add-user-button" class="add-user-row__item" @click="addInput">
<div :class="{ 'inactive-image': isMaxInputsCount }">
<AddFieldIcon class="add-user-row__item__image" />
</div>
<p class="add-user-row__item__label" :class="{ 'inactive-label': isMaxInputsCount }">Add More</p>
</div>
</div>
<div class="add-user__form-container__button-container">
<VButton
label="Cancel"
width="205px"
height="48px"
:on-press="onClose"
is-transparent="true"
/>
<VButton
label="Add Team Members"
width="205px"
height="48px"
:on-press="onAddUsersClick"
:is-disabled="!isButtonActive"
/>
</div>
<div class="notification-wrap">
<AddMemberNotificationIcon class="notification-wrap__image" />
<div class="notification-wrap__text-area">
<p class="notification-wrap__text-area__text">
If the team member you want to invite to join the project is still not on this Satellite, please
share this link to the signup page and ask them to register here:
<router-link target="_blank" rel="noopener noreferrer" exact to="/signup">
{{ registerPath }}
</router-link>
</p>
</div>
</div>
<div class="add-user__close-cross-container" @click="onClose">
<CloseCrossIcon />
</div>
</div>
<div class="notification-wrap">
<AddMemberNotificationIcon class="notification-wrap__image" />
<div class="notification-wrap__text-area">
<p class="notification-wrap__text-area__text">
If the team member you want to invite to join the project is still not on this Satellite, please
share this link to the signup page and ask them to register here:
<router-link target="_blank" rel="noopener noreferrer" exact to="/signup">
{{ registerPath }}
</router-link>
</p>
</div>
</div>
</div>
</div>
</template>
</VModal>
</template>
<script lang='ts'>
import { Component, Vue } from 'vue-property-decorator';
import VButton from '@/components/common/VButton.vue';
import { RouteConfig } from '@/router';
import { EmailInput } from '@/types/EmailInput';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
import { Validator } from '@/utils/validation';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
import AddFieldIcon from '@/../static/images/team/addField.svg';
import AddMemberNotificationIcon from '@/../static/images/team/addMemberNotification.svg';
import DeleteFieldIcon from '@/../static/images/team/deleteField.svg';
import { RouteConfig } from '@/router';
import { EmailInput } from '@/types/EmailInput';
import { APP_STATE_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
import { Validator } from '@/utils/validation';
// @vue/component
@Component({
components: {
VButton,
VModal,
ErrorIcon,
DeleteFieldIcon,
AddFieldIcon,
CloseCrossIcon,
AddMemberNotificationIcon,
},
})
export default class AddUserPopup extends Vue {
export default class AddTeamMemberModal extends Vue {
/**
* Initial empty inputs set.
*/
@ -196,7 +196,7 @@ export default class AddUserPopup extends Vue {
await this.$notify.error(`Unable to fetch project members. ${error.message}`);
}
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
this.closeModal();
this.isLoading = false;
}
@ -224,10 +224,10 @@ export default class AddUserPopup extends Vue {
}
/**
* Closes popup.
* Closes modal.
*/
public onClose(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
public closeModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
}
/**
@ -284,100 +284,19 @@ export default class AddUserPopup extends Vue {
</script>
<style scoped lang='scss'>
.add-user-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgb(134 134 148 / 40%);
z-index: 1121;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: 'font_regular', sans-serif;
}
.add-user-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px 0 50px;
&__item {
display: flex;
align-items: center;
justify-content: space-between;
&__image {
margin-right: 20px;
}
&__label {
font-family: 'font_medium', sans-serif;
font-size: 16px;
margin-left: 0;
padding-left: 0;
margin-block-start: 0;
margin-block-end: 0;
}
&:first-child {
cursor: pointer;
}
}
}
.inactive-label {
cursor: default;
color: #dadde5;
}
.error-input {
border: 1px solid red !important;
}
.inactive-image {
cursor: default;
.add-user-row__item__image {
&__rect {
fill: #dadde5;
}
&__path {
fill: #acb0bc;
}
}
}
.input-container.full-input {
width: 100%;
}
.red {
background-color: #eb5757;
}
.add-user {
width: 100%;
max-width: 1200px;
height: auto;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
justify-content: center;
font-family: 'font_regular', sans-serif;
&__main {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
display: flex;
flex-direction: row;
align-items: flex-start;
position: relative;
justify-content: center;
background-color: #fff;
padding: 80px 20px 80px 30px;
@ -475,6 +394,7 @@ export default class AddUserPopup extends Vue {
font-size: 16px;
line-height: 25px;
padding-left: 50px;
text-align: left;
}
&__button-container {
@ -486,24 +406,70 @@ export default class AddUserPopup extends Vue {
padding: 0 80px 0 50px;
}
}
}
&__close-cross-container {
.add-user-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 80px 0 50px;
&__item {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 30px;
top: 40px;
height: 24px;
width: 24px;
cursor: pointer;
justify-content: space-between;
&:hover .close-cross-svg-path {
fill: #2683ff;
&__image {
margin-right: 20px;
}
&__label {
font-family: 'font_medium', sans-serif;
font-size: 16px;
margin-left: 0;
padding-left: 0;
margin-block-start: 0;
margin-block-end: 0;
}
&:first-child {
cursor: pointer;
}
}
}
.inactive-label {
cursor: default;
color: #dadde5;
}
.error-input {
border: 1px solid red !important;
}
.inactive-image {
cursor: default;
.add-user-row__item__image {
&__rect {
fill: #dadde5;
}
&__path {
fill: #acb0bc;
}
}
}
.input-container.full-input {
width: 100%;
}
.red {
background-color: #eb5757;
}
.notification-wrap {
background-color: rgb(194 214 241 / 100%);
height: 98px;
@ -526,6 +492,7 @@ export default class AddUserPopup extends Vue {
&__text {
font-family: 'font_medium', sans-serif;
font-size: 16px;
text-align: left;
}
}
}
@ -533,38 +500,4 @@ export default class AddUserPopup extends Vue {
.scrollable {
overflow-y: scroll;
}
@media screen and (max-width: 1025px) {
.add-user {
padding: 10px;
max-width: 1000px;
&__main {
width: 100%;
padding-right: 0;
padding-left: 0;
}
&__info-panel-container {
display: none;
}
&__form-container {
max-width: 800px;
}
&-row__item {
width: 80%;
}
}
#add-user-button {
justify-content: flex-start;
.add-user-row__item__image {
padding-right: 20px;
}
}
}
</style>

View File

@ -7,6 +7,12 @@
<CreateProjectModal v-if="isCreateProjectModal" />
<AddPaymentMethodModal v-if="isAddPMModal" />
<OpenBucketModal v-if="isOpenBucketModal" />
<MFARecoveryCodesModal v-if="isMFARecoveryModal" />
<EnableMFAModal v-if="isEnableMFAModal" />
<DisableMFAModal v-if="isDisableMFAModal" />
<EditProfileModal v-if="isEditProfileModal" />
<ChangePasswordModal v-if="isChangePasswordModal" />
<AddTeamMemberModal v-if="isAddTeamMembersModal" />
</div>
</template>
@ -17,6 +23,12 @@ import CreateProjectPromptModal from '@/components/modals/CreateProjectPromptMod
import CreateProjectModal from '@/components/modals/CreateProjectModal.vue'
import AddPaymentMethodModal from "@/components/modals/AddPaymentMethodModal.vue";
import OpenBucketModal from "@/components/modals/OpenBucketModal.vue";
import MFARecoveryCodesModal from "@/components/modals/MFARecoveryCodesModal.vue";
import EnableMFAModal from "@/components/modals/EnableMFAModal.vue";
import DisableMFAModal from "@/components/modals/DisableMFAModal.vue";
import EditProfileModal from "@/components/modals/EditProfileModal.vue";
import ChangePasswordModal from "@/components/modals/ChangePasswordModal.vue";
import AddTeamMemberModal from "@/components/modals/AddTeamMemberModal.vue";
// @vue/component
@Component({
@ -25,6 +37,12 @@ import OpenBucketModal from "@/components/modals/OpenBucketModal.vue";
CreateProjectModal,
AddPaymentMethodModal,
OpenBucketModal,
MFARecoveryCodesModal,
EnableMFAModal,
DisableMFAModal,
EditProfileModal,
ChangePasswordModal,
AddTeamMemberModal,
},
})
export default class AllModals extends Vue {
@ -55,5 +73,47 @@ export default class AllModals extends Vue {
public get isOpenBucketModal(): boolean {
return this.$store.state.appStateModule.appState.isOpenBucketModalShown;
}
/**
* Indicates if MFA recovery modal is shown.
*/
public get isMFARecoveryModal(): boolean {
return this.$store.state.appStateModule.appState.isMFARecoveryModalShown;
}
/**
* Indicates if enable MFA modal is shown.
*/
public get isEnableMFAModal(): boolean {
return this.$store.state.appStateModule.appState.isEnableMFAModalShown;
}
/**
* Indicates if disable MFA modal is shown.
*/
public get isDisableMFAModal(): boolean {
return this.$store.state.appStateModule.appState.isDisableMFAModalShown;
}
/**
* Indicates if edit profile modal is shown.
*/
public get isEditProfileModal(): boolean {
return this.$store.state.appStateModule.appState.isEditProfileModalShown;
}
/**
* Indicates if change password modal is shown.
*/
public get isChangePasswordModal(): boolean {
return this.$store.state.appStateModule.appState.isChangePasswordModalShown;
}
/**
* Indicates if add team members modal is shown.
*/
public get isAddTeamMembersModal(): boolean {
return this.$store.state.appStateModule.appState.isAddTeamMembersModalShown;
}
}
</script>

View File

@ -2,12 +2,12 @@
// See LICENSE for copying information.
<template>
<div class="change-password-popup-container">
<div class="change-password-popup">
<div class="change-password-popup__form-container">
<div class="change-password-row-container">
<ChangePasswordIcon class="change-password-popup__form-container__svg" />
<h2 class="change-password-popup__form-container__main-label-text">Change Password</h2>
<VModal :on-close="closeModal">
<template #content>
<div class="change-password">
<div class="change-password__row">
<ChangePasswordIcon />
<h2 class="change-password__row__label">Change Password</h2>
</div>
<VInput
class="full-input"
@ -41,12 +41,12 @@
:error="confirmationPasswordError"
@setData="setPasswordConfirmation"
/>
<div class="change-password-popup__form-container__button-container">
<div class="change-password__buttons">
<VButton
label="Cancel"
width="205px"
height="48px"
:on-press="onCloseClick"
:on-press="closeModal"
is-transparent="true"
/>
<VButton
@ -57,30 +57,26 @@
/>
</div>
</div>
<div class="change-password-popup__close-cross-container" @click="onCloseClick">
<CloseCrossIcon />
</div>
</div>
</div>
</template>
</VModal>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VInput from '@/components/common/VInput.vue';
import PasswordStrength from '@/components/common/PasswordStrength.vue';
import VButton from '@/components/common/VButton.vue';
import ChangePasswordIcon from '@/../static/images/account/changePasswordPopup/changePassword.svg';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import { AuthHttpApi } from '@/api/auth';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { Validator } from '@/utils/validation';
import { RouteConfig } from "@/router";
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import PasswordStrength from '@/components/common/PasswordStrength.vue';
import VInput from '@/components/common/VInput.vue';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
import ChangePasswordIcon from '@/../static/images/account/changePasswordPopup/changePassword.svg';
const DELAY_BEFORE_REDIRECT = 2000; // 2 sec
@ -88,13 +84,13 @@ const DELAY_BEFORE_REDIRECT = 2000; // 2 sec
@Component({
components: {
ChangePasswordIcon,
CloseCrossIcon,
VInput,
VButton,
VModal,
PasswordStrength,
},
})
export default class ChangePasswordPopup extends Vue {
export default class ChangePasswordModal extends Vue {
private oldPassword = '';
private newPassword = '';
private confirmationPassword = '';
@ -112,24 +108,39 @@ export default class ChangePasswordPopup extends Vue {
*/
public isPasswordStrengthShown = false;
/**
* Enables password strength info container.
*/
public showPasswordStrength(): void {
this.isPasswordStrengthShown = true;
}
/**
* Disables password strength info container.
*/
public hidePasswordStrength(): void {
this.isPasswordStrengthShown = false;
}
/**
* Sets old password from input.
*/
public setOldPassword(value: string): void {
this.oldPassword = value;
this.oldPasswordError = '';
}
/**
* Sets new password from input.
*/
public setNewPassword(value: string): void {
this.newPassword = value;
this.newPasswordError = '';
}
/**
* Sets password confirmation from input.
*/
public setPasswordConfirmation(value: string): void {
this.confirmationPassword = value;
this.confirmationPasswordError = '';
@ -184,106 +195,46 @@ export default class ChangePasswordPopup extends Vue {
this.analytics.eventTriggered(AnalyticsEvent.PASSWORD_CHANGED);
await this.$notify.success('Password successfully changed!');
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
this.closeModal();
}
/**
* Closes popup.
*/
public onCloseClick(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
public closeModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_MODAL_SHOWN);
}
}
</script>
<style scoped lang="scss">
.change-password-popup-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgb(134 134 148 / 40%);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
font-family: 'font_regular', sans-serif;
}
.full-input {
margin-bottom: 15px;
}
.change-password-row-container {
width: 100%;
display: flex;
flex-direction: row;
align-content: center;
justify-content: flex-start;
margin-bottom: 40px;
}
.change-password-popup {
width: 100%;
max-width: 440px;
max-height: 470px;
.change-password {
background-color: #fff;
border-radius: 6px;
display: flex;
flex-direction: row;
align-items: flex-start;
position: relative;
justify-content: center;
padding: 80px;
flex-direction: column;
padding: 48px;
&__info-panel-container {
&__row {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
margin-right: 100px;
margin-top: 20px;
}
margin-bottom: 20px;
&__form-container {
width: 100%;
max-width: 440px;
&__main-label-text {
&__label {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 60px;
color: #384b65;
margin-bottom: 0;
margin-top: 0;
margin-left: 32px;
}
&__button-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 32px;
margin: 0 0 0 32px;
}
}
&__close-cross-container {
&__buttons {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 30px;
top: 40px;
height: 24px;
width: 24px;
cursor: pointer;
&:hover .close-cross-svg-path {
fill: #2683ff;
}
margin-top: 32px;
column-gap: 20px;
}
}
@ -292,20 +243,7 @@ export default class ChangePasswordPopup extends Vue {
width: 100%;
}
@media screen and (max-width: 720px) {
.change-password-popup {
&__info-panel-container {
display: none;
}
&__form-container {
&__button-container {
width: 100%;
}
}
}
.full-input {
margin-bottom: 15px;
}
</style>

View File

@ -186,7 +186,7 @@ export default class CreateProjectModal extends Vue {
<style scoped lang="scss">
.modal {
width: 400px;
padding: 50px 65px 65px;
padding: 54px 48px 51px;
display: flex;
align-items: center;
flex-direction: column;

View File

@ -2,56 +2,55 @@
// See LICENSE for copying information.
<template>
<div class="disable-mfa">
<div class="disable-mfa__container">
<h1 class="disable-mfa__container__title">Two-Factor Authentication</h1>
<p class="disable-mfa__container__subtitle">
Enter code from your favorite TOTP app to disable 2FA.
</p>
<div class="disable-mfa__container__confirm">
<h2 class="disable-mfa__container__confirm__title">Confirm Authentication Code</h2>
<ConfirmMFAInput ref="mfaInput" :on-input="onConfirmInput" :is-error="isError" :is-recovery="isRecoveryCodeState" />
<span class="disable-mfa__container__confirm__toggle" @click="toggleRecoveryCodeState">
Or use {{ isRecoveryCodeState ? '2FA code' : 'recovery code' }}
</span>
<VModal :on-close="closeModal">
<template #content>
<div class="disable-mfa">
<h1 class="disable-mfa__title">Two-Factor Authentication</h1>
<p class="disable-mfa__subtitle">
Enter code from your favorite TOTP app to disable 2FA.
</p>
<div class="disable-mfa__confirm">
<h2 class="disable-mfa__confirm__title">Confirm Authentication Code</h2>
<ConfirmMFAInput ref="mfaInput" :on-input="onConfirmInput" :is-error="isError" :is-recovery="isRecoveryCodeState" />
<span class="disable-mfa__confirm__toggle" @click="toggleRecoveryCodeState">
Or use {{ isRecoveryCodeState ? '2FA code' : 'recovery code' }}
</span>
</div>
<p class="disable-mfa__info">
After disabling 2FA, remove the authentication code from your TOTP app.
</p>
<div class="disable-mfa__buttons">
<VButton
class="disable-mfa__buttons__cancel-button"
label="Cancel"
width="50%"
height="44px"
is-white="true"
:on-press="closeModal"
/>
<VButton
label="Disable 2FA"
width="50%"
height="44px"
:on-press="disable"
:is-disabled="!(request.recoveryCode || request.passcode) || isLoading"
/>
</div>
</div>
<p class="disable-mfa__container__info">
After disabling 2FA, remove the authentication code from your TOTP app.
</p>
<div class="disable-mfa__container__buttons">
<VButton
class="cancel-button"
label="Cancel"
width="50%"
height="44px"
is-white="true"
:on-press="toggleModal"
/>
<VButton
label="Disable 2FA"
width="50%"
height="44px"
:on-press="disable"
:is-disabled="!(request.recoveryCode || request.passcode) || isLoading"
/>
</div>
<div class="disable-mfa__container__close-container" @click="toggleModal">
<CloseCrossIcon />
</div>
</div>
</div>
</template>
</VModal>
</template>
<script lang="ts">
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';
import { Component, Vue } from 'vue-property-decorator';
import { USER_ACTIONS } from '@/store/modules/users';
import { DisableMFARequest } from '@/types/users';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import ConfirmMFAInput from '@/components/account/mfa/ConfirmMFAInput.vue';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
interface ClearInput {
clearInput(): void;
@ -61,14 +60,11 @@ interface ClearInput {
@Component({
components: {
ConfirmMFAInput,
CloseCrossIcon,
VButton,
VModal,
},
})
export default class DisableMFAPopup extends Vue {
@Prop({default: () => () => {}})
public readonly toggleModal: () => void;
export default class DisableMFAModal extends Vue {
public isError = false;
public isLoading = false;
public request = new DisableMFARequest();
@ -78,6 +74,13 @@ export default class DisableMFAPopup extends Vue {
mfaInput: ConfirmMFAInput & ClearInput;
}
/**
* Closes disable MFA modal.
*/
public closeModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_DISABLE_MFA_MODAL_SHOWN);
}
/**
* Sets confirmation passcode value from input.
*/
@ -110,7 +113,7 @@ export default class DisableMFAPopup extends Vue {
await this.$notify.success('MFA was disabled successfully');
this.toggleModal();
this.closeModal();
} catch (error) {
await this.$notify.error(error.message);
this.isError = true;
@ -122,22 +125,8 @@ export default class DisableMFAPopup extends Vue {
</script>
<style scoped lang="scss">
.disable-mfa {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
display: flex;
justify-content: center;
z-index: 1000;
background: rgb(27 37 51 / 75%);
&__container {
.disable-mfa {
padding: 60px;
height: fit-content;
margin-top: 100px;
position: relative;
background: #fff;
border-radius: 6px;
display: flex;
@ -202,35 +191,10 @@ export default class DisableMFAPopup extends Vue {
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;
}
}
}
}
.cancel-button {
margin-right: 15px;
}
@media screen and (max-height: 750px) {
.disable-mfa {
padding-bottom: 20px;
overflow-y: scroll;
}
}
</style>

View File

@ -2,14 +2,14 @@
// See LICENSE for copying information.
<template>
<div class="edit-profile-popup-container">
<div class="edit-profile-popup">
<div class="edit-profile-popup__form-container">
<div class="edit-profile-row-container">
<div class="edit-profile-popup__form-container__avatar">
<h1 class="edit-profile-popup__form-container__avatar__letter">{{ avatarLetter }}</h1>
<VModal :on-close="closeModal">
<template #content>
<div class="edit-profile">
<div class="edit-profile__row">
<div class="edit-profile__row__avatar">
<h1 class="edit-profile__row__avatar__letter">{{ avatarLetter }}</h1>
</div>
<h2 class="edit-profile-popup__form-container__main-label-text">Edit Profile</h2>
<h2 class="edit-profile__row__label">Edit Profile</h2>
</div>
<VInput
label="Full Name"
@ -18,27 +18,24 @@
:init-value="userInfo.fullName"
@setData="setFullName"
/>
<div class="edit-profile-popup__form-container__button-container">
<div class="edit-profile__buttons">
<VButton
label="Cancel"
width="205px"
width="100%"
height="48px"
:on-press="onCloseClick"
:on-press="closeModal"
is-transparent="true"
/>
<VButton
label="Update"
width="205px"
width="100%"
height="48px"
:on-press="onUpdateClick"
/>
</div>
</div>
<div class="edit-profile-popup__close-cross-container" @click="onCloseClick">
<CloseCrossIcon />
</div>
</div>
</div>
</template>
</VModal>
</template>
<script lang="ts">
@ -46,37 +43,38 @@ import { Component, Vue } from 'vue-property-decorator';
import VInput from '@/components/common/VInput.vue';
import VButton from '@/components/common/VButton.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import VModal from '@/components/common/VModal.vue';
import { USER_ACTIONS } from '@/store/modules/users';
import { UpdatedUser } from '@/types/users';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
// @vue/component
@Component({
components: {
CloseCrossIcon,
VInput,
VButton,
VModal,
},
})
export default class EditProfilePopup extends Vue {
export default class EditProfileModal extends Vue {
private fullNameError = '';
private readonly userInfo: UpdatedUser =
new UpdatedUser(this.$store.getters.user.fullName, this.$store.getters.user.shortName);
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
/**
* Set full name value from input.
*/
public setFullName(value: string): void {
this.userInfo.setFullName(value);
this.fullNameError = '';
}
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
/**
* Validates name and tries to update user info and close popup.
*/
@ -99,14 +97,14 @@ export default class EditProfilePopup extends Vue {
await this.$notify.success('Account info successfully updated!');
await this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
this.closeModal();
}
/**
* Closes popup.
* Closes modal.
*/
public onCloseClick(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
public closeModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_EDIT_PROFILE_MODAL_SHOWN);
}
/**
@ -119,54 +117,17 @@ export default class EditProfilePopup extends Vue {
</script>
<style scoped lang="scss">
.edit-profile-row-container {
width: 100%;
display: flex;
flex-direction: row;
align-content: center;
justify-content: flex-start;
margin-bottom: 40px;
}
.edit-profile-popup-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgb(134 134 148 / 40%);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
font-family: 'font_regular', sans-serif;
}
.edit-profile-popup {
width: 100%;
max-width: 440px;
.edit-profile {
background-color: #fff;
border-radius: 6px;
display: flex;
flex-direction: row;
align-items: flex-start;
position: relative;
justify-content: center;
padding: 80px;
flex-direction: column;
padding: 48px;
&__info-panel-container {
&__row {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
margin-right: 100px;
margin-top: 20px;
}
&__form-container {
width: 100%;
max-width: 440px;
margin-top: 10px;
margin-bottom: 30px;
&__avatar {
width: 60px;
@ -186,55 +147,21 @@ export default class EditProfilePopup extends Vue {
}
}
&__main-label-text {
&__label {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 60px;
color: #384b65;
margin-top: 0;
}
&__button-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
}
&__close-cross-container {
&__buttons {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 30px;
top: 40px;
height: 24px;
width: 24px;
cursor: pointer;
&:hover .close-cross-svg-path {
fill: #2683ff;
}
}
}
@media screen and (max-width: 720px) {
.edit-profile-popup {
&__info-panel-container {
display: none;
}
&__form-container {
&__button-container {
width: 100%;
}
}
margin-top: 40px;
column-gap: 20px;
}
}
</style>

View File

@ -0,0 +1,311 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="enable-mfa">
<h1 class="enable-mfa__title">Two-Factor Authentication</h1>
<p v-if="isScan" class="enable-mfa__subtitle">
Scan this QR code in your favorite TOTP app to get started.
</p>
<p v-if="isEnable" class="enable-mfa__subtitle max-width">
Enter the authentication code generated in your TOTP app to confirm your account is connected.
</p>
<p v-if="isCodes" class="enable-mfa__subtitle">
Save recovery codes.
</p>
<div v-if="isScan" class="enable-mfa__scan">
<h2 class="enable-mfa__scan__title">Scan this QR Code</h2>
<p class="enable-mfa__scan__subtitle">Scan the following QR code in your OTP app.</p>
<div class="enable-mfa__scan__qr">
<canvas ref="canvas" class="enable-mfa__scan__qr__canvas" />
</div>
<p class="enable-mfa__scan__subtitle">Unable to scan? Use the following code instead:</p>
<p class="enable-mfa__scan__secret">{{ userMFASecret }}</p>
</div>
<div v-if="isEnable" class="enable-mfa__confirm">
<h2 class="enable-mfa__confirm__title">Confirm Authentication Code</h2>
<ConfirmMFAInput :on-input="onConfirmInput" :is-error="isError" />
</div>
<div v-if="isCodes" class="enable-mfa__codes">
<h2 class="enable-mfa__codes__title max-width">
Please save these codes somewhere to be able to recover access to your account.
</h2>
<p
v-for="(code, index) in userMFARecoveryCodes"
:key="index"
>
{{ code }}
</p>
</div>
<div class="enable-mfa__buttons">
<VButton
v-if="!isCodes"
class="cancel-button"
label="Cancel"
width="100%"
height="44px"
is-white="true"
:on-press="closeModal"
/>
<VButton
v-if="isScan"
label="Continue"
width="100%"
height="44px"
:on-press="showEnable"
/>
<VButton
v-if="isEnable"
label="Enable"
width="100%"
height="44px"
:on-press="enable"
:is-disabled="!confirmPasscode || isLoading"
/>
<VButton
v-if="isCodes"
label="Done"
width="100%"
height="44px"
:on-press="closeModal"
/>
</div>
</div>
</template>
</VModal>
</template>
<script lang="ts">
import QRCode from 'qrcode';
import { Component, Vue } from 'vue-property-decorator';
import { USER_ACTIONS } from '@/store/modules/users';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import ConfirmMFAInput from '@/components/account/mfa/ConfirmMFAInput.vue';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
// @vue/component
@Component({
components: {
ConfirmMFAInput,
VButton,
VModal,
},
})
export default class EnableMFAModal extends Vue {
public readonly qrLink =
`otpauth://totp/${encodeURIComponent(this.$store.getters.user.email)}?secret=${this.userMFASecret}&issuer=${encodeURIComponent(`STORJ ${this.satellite}`)}&algorithm=SHA1&digits=6&period=30`;
public isScan = true;
public isEnable = false;
public isCodes = false;
public isError = false;
public isLoading = false;
public confirmPasscode = '';
public $refs!: {
canvas: HTMLCanvasElement;
};
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
/**
* 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;
}
/**
* Closes enable MFA modal.
*/
public closeModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ENABLE_MFA_MODAL_SHOWN);
}
/**
* Toggles view to MFA Recovery Codes state.
*/
public async showCodes(): Promise<void> {
await this.$store.dispatch(USER_ACTIONS.GENERATE_USER_MFA_RECOVERY_CODES);
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 {
await this.$store.dispatch(USER_ACTIONS.ENABLE_USER_MFA, this.confirmPasscode);
await this.$store.dispatch(USER_ACTIONS.GET);
await this.showCodes();
this.analytics.eventTriggered(AnalyticsEvent.MFA_ENABLED);
await this.$notify.success('MFA was enabled successfully');
} catch (error) {
await this.$notify.error(error.message);
this.isError = true;
}
this.isLoading = false;
}
/**
* Returns satellite name from store.
*/
private get satellite(): string {
return this.$store.state.appStateModule.satelliteName;
}
/**
* Returns pre-generated MFA secret from store.
*/
private get userMFASecret(): string {
return this.$store.state.usersModule.userMFASecret;
}
/**
* Returns user MFA recovery codes from store.
*/
private get userMFARecoveryCodes(): string[] {
return this.$store.state.usersModule.userMFARecoveryCodes;
}
}
</script>
<style scoped lang="scss">
.enable-mfa {
padding: 60px;
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;
}
&__subtitle {
font-size: 16px;
line-height: 21px;
text-align: center;
color: #000;
margin: 0 0 45px;
}
&__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;
}
&__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;
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;
}
}
.cancel-button {
margin-right: 15px;
}
.max-width {
max-width: 485px;
}
</style>

View File

@ -0,0 +1,106 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="recovery">
<h1 class="recovery__title">Two-Factor Authentication</h1>
<div class="recovery__codes">
<p class="recovery__codes__subtitle">
Please save these codes somewhere to be able to recover access to your account.
</p>
<p
v-for="(code, index) in userMFARecoveryCodes"
:key="index"
>
{{ code }}
</p>
</div>
<VButton
class="recovery__done-button"
label="Done"
width="100%"
height="44px"
:on-press="closeModal"
/>
</div>
</template>
</VModal>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import VButton from '@/components/common/VButton.vue';
import VModal from "@/components/common/VModal.vue";
// @vue/component
@Component({
components: {
VButton,
VModal,
},
})
export default class MFARecoveryCodesModal extends Vue {
/**
* Closes modal.
*/
public closeModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_MFA_RECOVERY_MODAL_SHOWN);
}
/**
* Returns MFA recovery codes from store.
*/
public get userMFARecoveryCodes(): string[] {
return this.$store.state.usersModule.userMFARecoveryCodes;
}
}
</script>
<style scoped lang="scss">
.recovery {
padding: 60px;
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;
}
&__codes {
padding: 25px;
background: #f5f6fa;
border-radius: 6px;
width: calc(100% - 50px);
display: flex;
flex-direction: column;
align-items: center;
&__subtitle {
font-size: 16px;
line-height: 21px;
text-align: center;
color: #000;
margin: 0 0 30px;
max-width: 485px;
}
}
&__done-button {
margin-top: 30px;
}
}
</style>

View File

@ -28,7 +28,7 @@
label="+ Add"
width="122px"
height="48px"
:on-press="onAddUsersClick"
:on-press="toggleTeamMembersModal"
:is-disabled="isAddButtonDisabled"
/>
</div>
@ -74,7 +74,6 @@
<div v-if="isDeleteClicked" class="blur-content" />
<div v-if="isDeleteClicked" class="blur-search" />
</div>
<AddUserPopup v-if="isAddTeamMembersPopupShown" />
</div>
</template>
@ -84,7 +83,6 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
import VButton from '@/components/common/VButton.vue';
import VHeader from '@/components/common/VHeader.vue';
import VInfo from '@/components/common/VInfo.vue';
import AddUserPopup from '@/components/team/AddUserPopup.vue';
import InfoIcon from '@/../static/images/team/infoTooltip.svg';
@ -92,9 +90,9 @@ import { RouteConfig } from '@/router';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { ProjectMemberHeaderState } from '@/types/projectMembers';
import { Project } from '@/types/projects';
import { APP_STATE_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
import { AnalyticsHttpApi } from '@/api/analytics';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
declare interface ClearSearch {
clearSearch(): void;
@ -105,7 +103,6 @@ declare interface ClearSearch {
components: {
VButton,
VHeader,
AddUserPopup,
VInfo,
InfoIcon,
},
@ -145,10 +142,10 @@ export default class HeaderArea extends Vue {
}
/**
* Opens add team members popup.
* Opens add team members modal.
*/
public onAddUsersClick(): void {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
public toggleTeamMembersModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
}
public onFirstDeleteClick(): void {
@ -197,13 +194,6 @@ export default class HeaderArea extends Vue {
}
}
/**
* Indicates if add team member popup should be rendered.
*/
public get isAddTeamMembersPopupShown(): boolean {
return this.$store.state.appStateModule.appState.isAddTeamMembersPopupShown;
}
public get isDefaultState(): boolean {
return this.headerState === 0;
}

View File

@ -10,7 +10,7 @@ import { AppState } from '@/utils/constants/appStateEnum';
class ViewsState {
constructor(
public fetchState = AppState.LOADING,
public isAddTeamMembersPopupShown = false,
public isAddTeamMembersModalShown = false,
public isAccountDropdownShown = false,
public isSelectProjectDropdownShown = false,
public isResourcesDropdownShown = false,
@ -24,8 +24,8 @@ class ViewsState {
public isAGDatePickerShown = false,
public isChartsDatePickerShown = false,
public isPermissionsDropdownShown = false,
public isEditProfilePopupShown = false,
public isChangePasswordPopupShown = false,
public isEditProfileModalShown = false,
public isChangePasswordModalShown = false,
public isPaymentSelectionShown = false,
public isUploadCancelPopupVisible = false,
public isSuccessfulPasswordResetShown = false,
@ -33,6 +33,9 @@ class ViewsState {
public isCreateProjectModalShown = false,
public isAddPMModalShown = false,
public isOpenBucketModalShown = false,
public isMFARecoveryModalShown = false,
public isEnableMFAModalShown = false,
public isDisableMFAModalShown = false,
public onbAGStepBackRoute = "",
public onbAPIKeyStepBackRoute = "",
@ -65,8 +68,8 @@ interface AppContext {
export const appStateModule = {
state: new State(),
mutations: {
[APP_STATE_MUTATIONS.TOGGLE_ADD_TEAMMEMBER_POPUP](state: State): void {
state.appState.isAddTeamMembersPopupShown = !state.appState.isAddTeamMembersPopupShown;
[APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL](state: State): void {
state.appState.isAddTeamMembersModalShown = !state.appState.isAddTeamMembersModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_ACCOUNT_DROPDOWN](state: State): void {
state.appState.isAccountDropdownShown = !state.appState.isAccountDropdownShown;
@ -110,11 +113,11 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET](state: State): void {
state.appState.isSuccessfulPasswordResetShown = !state.appState.isSuccessfulPasswordResetShown;
},
[APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_POPUP](state: State): void {
state.appState.isChangePasswordPopupShown = !state.appState.isChangePasswordPopupShown;
[APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_MODAL_SHOWN](state: State): void {
state.appState.isChangePasswordModalShown = !state.appState.isChangePasswordModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_EDIT_PROFILE_POPUP](state: State): void {
state.appState.isEditProfilePopupShown = !state.appState.isEditProfilePopupShown;
[APP_STATE_MUTATIONS.TOGGLE_EDIT_PROFILE_MODAL_SHOWN](state: State): void {
state.appState.isEditProfileModalShown = !state.appState.isEditProfileModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_UPLOAD_CANCEL_POPUP](state: State): void {
state.appState.isUploadCancelPopupVisible = !state.appState.isUploadCancelPopupVisible;
@ -131,6 +134,15 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.TOGGLE_OPEN_BUCKET_MODAL_SHOWN](state: State): void {
state.appState.isOpenBucketModalShown = !state.appState.isOpenBucketModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_MFA_RECOVERY_MODAL_SHOWN](state: State): void {
state.appState.isMFARecoveryModalShown = !state.appState.isMFARecoveryModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_ENABLE_MFA_MODAL_SHOWN](state: State): void {
state.appState.isEnableMFAModalShown = !state.appState.isEnableMFAModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_DISABLE_MFA_MODAL_SHOWN](state: State): void {
state.appState.isDisableMFAModalShown = !state.appState.isDisableMFAModalShown;
},
[APP_STATE_MUTATIONS.SHOW_SET_DEFAULT_PAYMENT_METHOD_POPUP](state: State, id: string): void {
state.appState.setDefaultPaymentMethodID = id;
},
@ -197,13 +209,6 @@ export const appStateModule = {
},
},
actions: {
[APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS]: function ({commit, state}: AppContext): void {
if (!state.appState.isAddTeamMembersPopupShown) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
}
commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAMMEMBER_POPUP);
},
[APP_STATE_ACTIONS.TOGGLE_ACCOUNT]: function ({commit, state}: AppContext): void {
if (!state.appState.isAccountDropdownShown) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
@ -309,15 +314,9 @@ export const appStateModule = {
commit(APP_STATE_MUTATIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
},
[APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP]: function ({commit}: AppContext): void {
commit(APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
},
[APP_STATE_ACTIONS.TOGGLE_UPLOAD_CANCEL_POPUP]: function ({commit}: AppContext): void {
commit(APP_STATE_MUTATIONS.TOGGLE_UPLOAD_CANCEL_POPUP);
},
[APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP]: function ({commit}: AppContext): void {
commit(APP_STATE_MUTATIONS.TOGGLE_EDIT_PROFILE_POPUP);
},
[APP_STATE_ACTIONS.SHOW_SET_DEFAULT_PAYMENT_METHOD_POPUP]: function ({commit, state}: AppContext, methodID: string): void {
if (!state.appState.setDefaultPaymentMethodID) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);

View File

@ -10,7 +10,7 @@ export const NOTIFICATION_MUTATIONS = {
};
export const APP_STATE_MUTATIONS = {
TOGGLE_ADD_TEAMMEMBER_POPUP: 'TOGGLE_ADD_TEAMMEMBER_POPUP',
TOGGLE_ADD_TEAM_MEMBERS_MODAL: 'TOGGLE_ADD_TEAM_MEMBERS_MODAL',
TOGGLE_ACCOUNT_DROPDOWN: 'TOGGLE_ACCOUNT_DROPDOWN',
TOGGLE_SELECT_PROJECT_DROPDOWN: 'TOGGLE_SELECT_PROJECT_DROPDOWN',
TOGGLE_RESOURCES_DROPDOWN: 'TOGGLE_RESOURCES_DROPDOWN',
@ -26,13 +26,16 @@ export const APP_STATE_MUTATIONS = {
TOGGLE_PERMISSIONS_DROPDOWN: 'TOGGLE_PERMISSIONS_DROPDOWN',
TOGGLE_SUCCESSFUL_PASSWORD_RESET: 'TOGGLE_SUCCESSFUL_PASSWORD_RESET',
TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP: 'TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP',
TOGGLE_EDIT_PROFILE_POPUP: 'TOGGLE_EDIT_PROFILE_POPUP',
TOGGLE_CHANGE_PASSWORD_POPUP: 'TOGGLE_CHANGE_PASSWORD_POPUP',
TOGGLE_EDIT_PROFILE_MODAL_SHOWN: 'TOGGLE_EDIT_PROFILE_MODAL_SHOWN',
TOGGLE_CHANGE_PASSWORD_MODAL_SHOWN: 'TOGGLE_CHANGE_PASSWORD_MODAL_SHOWN',
TOGGLE_UPLOAD_CANCEL_POPUP: 'TOGGLE_UPLOAD_CANCEL_POPUP',
TOGGLE_CREATE_PROJECT_PROMPT_POPUP: 'TOGGLE_CREATE_PROJECT_PROMPT_POPUP',
TOGGLE_CREATE_PROJECT_POPUP: 'TOGGLE_CREATE_PROJECT_POPUP',
TOGGLE_IS_ADD_PM_MODAL_SHOWN: 'TOGGLE_IS_ADD_PM_MODAL_SHOWN',
TOGGLE_OPEN_BUCKET_MODAL_SHOWN: 'TOGGLE_OPEN_BUCKET_MODAL_SHOWN',
TOGGLE_MFA_RECOVERY_MODAL_SHOWN: 'TOGGLE_MFA_RECOVERY_MODAL_SHOWN',
TOGGLE_ENABLE_MFA_MODAL_SHOWN: 'TOGGLE_ENABLE_MFA_MODAL_SHOWN',
TOGGLE_DISABLE_MFA_MODAL_SHOWN: 'TOGGLE_DISABLE_MFA_MODAL_SHOWN',
SHOW_DELETE_PAYMENT_METHOD_POPUP: 'SHOW_DELETE_PAYMENT_METHOD_POPUP',
SHOW_SET_DEFAULT_PAYMENT_METHOD_POPUP: 'SHOW_SET_DEFAULT_PAYMENT_METHOD_POPUP',
CLOSE_ALL: 'CLOSE_ALL',

View File

@ -2,7 +2,6 @@
// See LICENSE for copying information.
export const APP_STATE_ACTIONS = {
TOGGLE_TEAM_MEMBERS: 'toggleAddTeamMembersPopup',
TOGGLE_ACCOUNT: 'toggleAccountDropdown',
TOGGLE_SELECT_PROJECT_DROPDOWN: 'toggleSelectProjectDropdown',
TOGGLE_RESOURCES_DROPDOWN: 'toggleResourcesDropdown',
@ -18,8 +17,6 @@ export const APP_STATE_ACTIONS = {
TOGGLE_PERMISSIONS_DROPDOWN: 'togglePermissionsDropdown',
TOGGLE_SUCCESSFUL_PASSWORD_RESET: 'TOGGLE_SUCCESSFUL_PASSWORD_RESET',
TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP: 'toggleSuccessfulProjectCreationPopup',
TOGGLE_EDIT_PROFILE_POPUP: 'toggleEditProfilePopup',
TOGGLE_CHANGE_PASSWORD_POPUP: 'toggleChangePasswordPopup',
TOGGLE_UPLOAD_CANCEL_POPUP: 'toggleUploadCancelPopup',
SHOW_SET_DEFAULT_PAYMENT_METHOD_POPUP: 'showSetDefaultPaymentMethodPopup',
CLOSE_SET_DEFAULT_PAYMENT_METHOD_POPUP: 'closeSetDefaultPaymentMethodPopup',

View File

@ -32,7 +32,6 @@
</div>
</div>
</div>
<MFARecoveryCodesPopup v-if="isMFACodesPopup" :toggle-modal="toggleMFACodesPopup" />
<AllModals />
</div>
</template>
@ -44,7 +43,6 @@ import AllModals from "@/components/modals/AllModals.vue";
import PaidTierBar from '@/components/infoBars/PaidTierBar.vue';
import MFARecoveryCodeBar from '@/components/infoBars/MFARecoveryCodeBar.vue';
import BetaSatBar from '@/components/infoBars/BetaSatBar.vue';
import MFARecoveryCodesPopup from '@/components/account/mfa/MFARecoveryCodesPopup.vue';
import NavigationArea from '@/components/navigation/NavigationArea.vue';
import ProjectInfoBar from "@/components/infoBars/ProjectInfoBar.vue";
@ -86,7 +84,6 @@ const {
MFARecoveryCodeBar,
BetaSatBar,
ProjectInfoBar,
MFARecoveryCodesPopup,
},
})
export default class DashboardArea extends Vue {
@ -97,8 +94,6 @@ export default class DashboardArea extends Vue {
// Minimum number of recovery codes before the recovery code warning bar is shown.
public recoveryCodeWarningThreshold = 4;
public isMFACodesPopup = false;
public readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
/**
@ -180,12 +175,19 @@ export default class DashboardArea extends Vue {
public async generateNewMFARecoveryCodes(): Promise<void> {
try {
await this.$store.dispatch(USER_ACTIONS.GENERATE_USER_MFA_RECOVERY_CODES);
this.toggleMFACodesPopup();
this.toggleMFARecoveryModal();
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Toggles MFA recovery modal visibility.
*/
public toggleMFARecoveryModal(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_MFA_RECOVERY_MODAL_SHOWN);
}
/**
* Opens add payment method modal.
*/
@ -193,13 +195,6 @@ export default class DashboardArea extends Vue {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_IS_ADD_PM_MODAL_SHOWN);
}
/**
* Toggles MFA recovery codes popup visibility.
*/
public toggleMFACodesPopup(): void {
this.isMFACodesPopup = !this.isMFACodesPopup;
}
/**
* Checks if stored project is in fetched projects array and selects it.
* Selects first fetched project if check is not successful.

View File

@ -9,7 +9,7 @@ import { appStateModule } from '@/store/modules/appState';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { makeProjectMembersModule } from '@/store/modules/projectMembers';
import { ProjectMember, ProjectMemberHeaderState, ProjectMembersPage } from '@/types/projectMembers';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { APP_STATE_MUTATIONS } from "@/store/mutationConstants";
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ProjectMembersApiMock } from '../mock/api/projectMembers';
@ -50,27 +50,24 @@ describe('Team HeaderArea', () => {
});
it('renders correctly with opened Add team member popup', () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
const wrapper = shallowMount<HeaderArea>(HeaderArea, {
store,
localVue,
});
const addNewTemMemberPopup = wrapper.findAll('adduserpopup-stub');
expect(wrapper).toMatchSnapshot();
expect(addNewTemMemberPopup.length).toBe(1);
expect(wrapper.findAll('.header-default-state').length).toBe(1);
expect(wrapper.vm.isDeleteClicked).toBe(false);
expect(wrapper.findAll('.blur-content').length).toBe(0);
expect(wrapper.findAll('.blur-search').length).toBe(0);
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
});
it('renders correctly with selected users', () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
const selectedUsersCount = 2;
@ -93,7 +90,7 @@ describe('Team HeaderArea', () => {
});
it('renders correctly with 2 selected users and delete clicked once', async () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
const selectedUsersCount = 2;
@ -120,7 +117,7 @@ describe('Team HeaderArea', () => {
});
it('renders correctly with 1 selected user and delete clicked once', async () => {
store.dispatch(APP_STATE_ACTIONS.TOGGLE_TEAM_MEMBERS);
store.commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAM_MEMBERS_MODAL);
const selectedUsersCount = 1;

View File

@ -17,7 +17,6 @@ exports[`Team HeaderArea renders correctly 1`] = `
<!---->
<!---->
</div>
<!---->
</div>
`;
@ -41,7 +40,6 @@ exports[`Team HeaderArea renders correctly with 1 selected user and delete click
<div class="blur-content"></div>
<div class="blur-search"></div>
</div>
<adduserpopup-stub></adduserpopup-stub>
</div>
`;
@ -65,7 +63,6 @@ exports[`Team HeaderArea renders correctly with 2 selected users and delete clic
<div class="blur-content"></div>
<div class="blur-search"></div>
</div>
<!---->
</div>
`;
@ -86,7 +83,6 @@ exports[`Team HeaderArea renders correctly with opened Add team member popup 1`]
<!---->
<!---->
</div>
<adduserpopup-stub></adduserpopup-stub>
</div>
`;
@ -108,6 +104,5 @@ exports[`Team HeaderArea renders correctly with selected users 1`] = `
<!---->
<!---->
</div>
<adduserpopup-stub></adduserpopup-stub>
</div>
`;

View File

@ -17,7 +17,6 @@ exports[`Dashboard renders correctly when data is loaded 1`] = `
</div>
</div>
</div>
<!---->
<allmodals-stub></allmodals-stub>
</div>
`;
@ -27,7 +26,6 @@ exports[`Dashboard renders correctly when data is loading 1`] = `
<div class="loading-overlay active">
<loaderimage-stub class="loading-icon"></loaderimage-stub>
</div>
<!---->
<allmodals-stub></allmodals-stub>
</div>
`;