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:
parent
e4b325537e
commit
6f11c8b32c
@ -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>
|
||||
|
247
web/satellite/src/components/modals/EnterPassphraseModal.vue
Normal file
247
web/satellite/src/components/modals/EnterPassphraseModal.vue
Normal 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>
|
@ -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);
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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 = '';
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user