web/satellite: show create/enter passphrase modal after login

Show create/enter passphrase modal after login for new project level passphrase flow.
Also fixed buckets view mounted hook to load create bucket modal instead of old flow.

Issue:
https://github.com/storj/storj/issues/5510

Change-Id: If9ea70faaa2987f336d72d55a6ed2bbd02ced592
This commit is contained in:
Vitalii 2023-01-26 17:00:09 +02:00
parent e4b325537e
commit 6f11c8b32c
8 changed files with 303 additions and 4 deletions

View File

@ -24,6 +24,7 @@
<CreateProjectPassphraseModal v-if="isCreateProjectPassphraseModal" />
<ManageProjectPassphraseModal v-if="isManageProjectPassphraseModal" />
<CreateBucketModal v-if="isCreateBucketModal" />
<EnterPassphraseModal v-if="isEnterPassphraseModal" />
</div>
</template>
@ -51,10 +52,12 @@ import AddCouponCodeModal from '@/components/modals/AddCouponCodeModal.vue';
import NewBillingAddCouponCodeModal from '@/components/modals/NewBillingAddCouponCodeModal.vue';
import CreateProjectPassphraseModal from '@/components/modals/createProjectPassphrase/CreateProjectPassphraseModal.vue';
import ManageProjectPassphraseModal from '@/components/modals/manageProjectPassphrase/ManageProjectPassphraseModal.vue';
import EnterPassphraseModal from '@/components/modals/EnterPassphraseModal.vue';
// @vue/component
@Component({
components: {
EnterPassphraseModal,
ManageProjectPassphraseModal,
CreateProjectPassphraseModal,
DeleteBucketModal,
@ -226,5 +229,12 @@ export default class AllModals extends Vue {
public get isCreateBucketModal(): boolean {
return this.$store.state.appStateModule.appState.isCreateBucketModalShown;
}
/**
* Indicates if enter passphrase modal is shown.
*/
public get isEnterPassphraseModal(): boolean {
return this.$store.state.appStateModule.appState.isEnterPassphraseModalShown;
}
}
</script>

View File

@ -0,0 +1,247 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<VModal :on-close="closeModal">
<template #content>
<div class="modal">
<EnterPassphraseIcon />
<h1 class="modal__title">Enter your encryption passphrase</h1>
<p class="modal__info">
To open a project and view your encrypted files, <br>please enter your encryption passphrase.
</p>
<VInput
label="Encryption Passphrase"
placeholder="Enter your passphrase"
:error="enterError"
role-description="passphrase"
is-password
:disabled="isLoading"
@setData="setPassphrase"
/>
<div class="modal__buttons">
<VButton
label="Enter without passphrase"
height="48px"
font-size="14px"
:is-transparent="true"
:on-press="closeModal"
:is-disabled="isLoading"
/>
<VButton
label="Continue ->"
height="48px"
font-size="14px"
:on-press="onContinue"
:is-disabled="isLoading"
/>
</div>
</div>
</template>
</VModal>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { OBJECTS_ACTIONS, OBJECTS_MUTATIONS } from '@/store/modules/objects';
import { MetaUtils } from '@/utils/meta';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { AnalyticsHttpApi } from '@/api/analytics';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import VModal from '@/components/common/VModal.vue';
import VInput from '@/components/common/VInput.vue';
import VButton from '@/components/common/VButton.vue';
import EnterPassphraseIcon from '@/../static/images/buckets/openBucket.svg';
// @vue/component
@Component({
components: {
VInput,
VModal,
VButton,
EnterPassphraseIcon,
},
})
export default class EnterPassphraseModal extends Vue {
private worker: Worker;
private readonly FILE_BROWSER_AG_NAME: string = 'Web file browser API key';
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
public enterError = '';
public passphrase = '';
public isLoading = false;
/**
* Lifecycle hook after initial render.
* Sets local worker.
*/
public mounted(): void {
this.setWorker();
}
/**
* Sets access and navigates to object browser.
*/
public async onContinue(): Promise<void> {
if (this.isLoading) return;
if (!this.passphrase) {
this.enterError = 'Passphrase can\'t be empty';
this.analytics.errorEventTriggered(AnalyticsErrorEventSource.OPEN_BUCKET_MODAL);
return;
}
this.isLoading = true;
try {
await this.setAccess();
this.isLoading = false;
this.closeModal();
} catch (error) {
await this.$notify.error(error.message, AnalyticsErrorEventSource.OPEN_BUCKET_MODAL);
this.isLoading = false;
}
}
/**
* Sets access to S3 client.
*/
public async setAccess(): Promise<void> {
if (!this.apiKey) {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.DELETE_BY_NAME_AND_PROJECT_ID, this.FILE_BROWSER_AG_NAME);
const cleanAPIKey: AccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, this.FILE_BROWSER_AG_NAME);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_API_KEY, cleanAPIKey.secret);
}
const now = new Date();
const inThreeDays = new Date(now.setDate(now.getDate() + 3));
await this.worker.postMessage({
'type': 'SetPermission',
'isDownload': true,
'isUpload': true,
'isList': true,
'isDelete': true,
'notAfter': inThreeDays.toISOString(),
'buckets': [],
'apiKey': this.apiKey,
});
const grantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
if (grantEvent.data.error) {
throw new Error(grantEvent.data.error);
}
const salt = await this.$store.dispatch(PROJECTS_ACTIONS.GET_SALT, this.$store.getters.selectedProject.id);
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': grantEvent.data.value,
'passphrase': this.passphrase,
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const accessGrantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
if (accessGrantEvent.data.error) {
throw new Error(accessGrantEvent.data.error);
}
const accessGrant = accessGrantEvent.data.value;
const gatewayCredentials: EdgeCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, { accessGrant });
await this.$store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT);
await this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false);
}
/**
* Sets local worker with worker instantiated in store.
*/
public setWorker(): void {
this.worker = this.$store.state.accessGrantsModule.accessGrantsWebWorker;
this.worker.onerror = (error: ErrorEvent) => {
this.$notify.error(error.message, AnalyticsErrorEventSource.OPEN_BUCKET_MODAL);
};
}
/**
* Closes open bucket modal.
*/
public closeModal(): void {
if (this.isLoading) return;
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ENTER_PASSPHRASE_MODAL_SHOWN);
}
/**
* Sets passphrase from child component.
*/
public setPassphrase(passphrase: string): void {
if (this.enterError) this.enterError = '';
this.passphrase = passphrase;
this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
}
/**
* Returns apiKey from store.
*/
private get apiKey(): string {
return this.$store.state.objectsModule.apiKey;
}
}
</script>
<style scoped lang="scss">
.modal {
font-family: 'font_regular', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 62px 62px 54px;
max-width: 500px;
@media screen and (max-width: 600px) {
padding: 62px 24px 54px;
}
&__title {
font-family: 'font_bold', sans-serif;
font-size: 26px;
line-height: 31px;
color: #131621;
margin: 30px 0 15px;
}
&__info {
font-size: 16px;
line-height: 21px;
text-align: center;
color: #354049;
margin-bottom: 32px;
}
&__buttons {
display: flex;
column-gap: 20px;
margin-top: 31px;
width: 100%;
@media screen and (max-width: 500px) {
flex-direction: column-reverse;
column-gap: unset;
row-gap: 20px;
}
}
}
</style>

View File

@ -101,7 +101,13 @@ export default class BucketsView extends Vue {
if (!this.bucketsPage.buckets.length && !wasDemoBucketCreated) {
this.analytics.pageVisit(RouteConfig.Buckets.with(RouteConfig.BucketCreation).path);
await this.$router.push(RouteConfig.Buckets.with(RouteConfig.BucketCreation).path);
if (this.isNewEncryptionPassphraseFlowEnabled) {
if (!this.promptForPassphrase) {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_CREATE_BUCKET_MODAL_SHOWN);
}
} else {
await this.$router.push(RouteConfig.Buckets.with(RouteConfig.BucketCreation).path);
}
}
} catch (error) {
await this.$notify.error(`Failed to setup Buckets view. ${error.message}`, AnalyticsErrorEventSource.BUCKET_PAGE);

View File

@ -264,8 +264,18 @@ export default class NewProjectDashboard extends Vue {
const past = new Date();
past.setDate(past.getDate() - 30);
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, { since: past, before: now });
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, this.$store.getters.selectedProject.id);
if (this.isNewEncryptionPassphraseFlowEnabled && this.hasJustLoggedIn) {
if (this.limits.objectCount > 0) {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ENTER_PASSPHRASE_MODAL_SHOWN);
} else {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_CREATE_PROJECT_PASSPHRASE_MODAL_SHOWN);
}
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_HAS_JUST_LOGGED_IN);
}
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, { since: past, before: now });
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
} catch (error) {
await this.$notify.error(error.message, AnalyticsErrorEventSource.PROJECT_DASHBOARD_PAGE);
@ -438,6 +448,13 @@ export default class NewProjectDashboard extends Vue {
return this.$store.state.appStateModule.isNewEncryptionPassphraseFlowEnabled;
}
/**
* Indicates if user has just logged in.
*/
public get hasJustLoggedIn(): boolean {
return this.$store.state.appStateModule.appState.hasJustLoggedIn;
}
/**
* Formats value to needed form and returns it.
*/

View File

@ -26,6 +26,7 @@ import { FilesState, makeFilesModule } from '@/store/modules/files';
import { NavigationLink } from '@/types/navigation';
import { ABTestingState, makeABTestingModule } from '@/store/modules/abTesting';
import { ABHttpApi } from '@/api/abtesting';
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
Vue.use(Vuex);
@ -99,7 +100,13 @@ export default store;
store and the router. Many of the tests require router, however, this implementation
relies on store state for the routing behavior.
*/
router.beforeEach(async (to, _, next) => {
router.beforeEach(async (to, from, next) => {
if (to.name === RouteConfig.NewProjectDashboard.name && from.name === RouteConfig.Login.name) {
if (store.state.appStateModule.isNewEncryptionPassphraseFlowEnabled) {
store.commit(APP_STATE_MUTATIONS.TOGGLE_HAS_JUST_LOGGED_IN);
}
}
if (!to.path.includes(RouteConfig.UploadFile.path) && !store.state.appStateModule.appState.isUploadCancelPopupVisible) {
const areUploadsInProgress: boolean = await store.dispatch(OBJECTS_ACTIONS.CHECK_ONGOING_UPLOADS, to.path);
if (areUploadsInProgress) return;

View File

@ -34,6 +34,7 @@ class ViewsState {
public isCreateBucketModalShown = false,
public isAddPMModalShown = false,
public isOpenBucketModalShown = false,
public isEnterPassphraseModalShown = false,
public isMFARecoveryModalShown = false,
public isEnableMFAModalShown = false,
public isDisableMFAModalShown = false,
@ -48,6 +49,7 @@ class ViewsState {
public isAddCouponModalShown = false,
public isNewBillingAddCouponModalShown = false,
public isBillingNotificationShown = true,
public hasJustLoggedIn = false,
public onbAGStepBackRoute = '',
public onbAPIKeyStepBackRoute = '',
@ -146,6 +148,12 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.TOGGLE_OPEN_BUCKET_MODAL_SHOWN](state: State): void {
state.appState.isOpenBucketModalShown = !state.appState.isOpenBucketModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_ENTER_PASSPHRASE_MODAL_SHOWN](state: State): void {
state.appState.isEnterPassphraseModalShown = !state.appState.isEnterPassphraseModalShown;
},
[APP_STATE_MUTATIONS.TOGGLE_HAS_JUST_LOGGED_IN](state: State): void {
state.appState.hasJustLoggedIn = !state.appState.hasJustLoggedIn;
},
[APP_STATE_MUTATIONS.TOGGLE_CREATE_PROJECT_PASSPHRASE_MODAL_SHOWN](state: State): void {
state.appState.isCreateProjectPassphraseModalShown = !state.appState.isCreateProjectPassphraseModalShown;
},
@ -234,6 +242,8 @@ export const appStateModule = {
state.appState.isCreateProjectPassphraseModalShown = false;
state.appState.isManageProjectPassphraseModalShown = false;
state.appState.isObjectDetailsModalShown = false;
state.appState.isEnterPassphraseModalShown = false;
state.appState.hasJustLoggedIn = false;
state.appState.isAddCouponModalShown = false;
state.appState.isNewBillingAddCouponModalShown = false;
state.appState.onbAGStepBackRoute = '';

View File

@ -33,6 +33,8 @@ export const APP_STATE_MUTATIONS = {
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_ENTER_PASSPHRASE_MODAL_SHOWN: 'TOGGLE_ENTER_PASSPHRASE_MODAL_SHOWN',
TOGGLE_HAS_JUST_LOGGED_IN: 'TOGGLE_HAS_JUST_LOGGED_IN',
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',

View File

@ -78,7 +78,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { RouteConfig } from '@/router';