web/satellite: create bucket without a passphrase feature

Allow user to create bucket without a passphrase if project level passphrase was not set.

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

Change-Id: Ifc4a6724229ce0708db720edb2f8953098e346ed
This commit is contained in:
Vitalii 2023-01-31 23:49:12 +02:00 committed by Vitalii Shpital
parent ae9ea22193
commit fa26ae85e9
7 changed files with 248 additions and 49 deletions

View File

@ -60,6 +60,11 @@ import { useNotify, useRouter, useStore } from '@/utils/hooks';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { Validator } from '@/utils/validation';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { MetaUtils } from '@/utils/meta';
import { LocalData } from '@/utils/localData';
import VLoader from '@/components/common/VLoader.vue';
import VInput from '@/components/common/VInput.vue';
@ -78,6 +83,9 @@ const bucketName = ref<string>('');
const nameError = ref<string>('');
const bucketNamesLoading = ref<boolean>(true);
const isLoading = ref<boolean>(false);
const worker = ref<Worker>();
const FILE_BROWSER_AG_NAME = 'Web file browser API key';
/**
* Returns all bucket names from store.
@ -86,12 +94,50 @@ const allBucketNames = computed((): string[] => {
return store.state.bucketUsageModule.allBucketNames;
});
/**
* Returns condition if user has to be prompt for passphrase from store.
*/
const promptForPassphrase = computed((): boolean => {
return store.state.objectsModule.promptForPassphrase;
});
/**
* Returns object browser api key from store.
*/
const apiKey = computed((): string => {
return store.state.objectsModule.apiKey;
});
/**
* Returns edge credentials for bucket creation from store.
*/
const gatewayCredentialsForCreate = computed((): EdgeCredentials => {
return store.state.objectsModule.gatewayCredentialsForCreate;
});
/**
* Indicates if bucket was created.
*/
const bucketWasCreated = computed((): boolean => {
const status = LocalData.getBucketWasCreatedStatus();
if (status !== null) {
return status;
}
return false;
});
/**
* Validates provided bucket's name and creates a bucket.
*/
async function onCreate(): Promise<void> {
if (isLoading.value) return;
if (!worker.value) {
notify.error('Worker is not defined', AnalyticsErrorEventSource.BUCKET_CREATION_NAME_STEP);
return;
}
if (!isBucketNameValid(bucketName.value)) {
analytics.errorEventTriggered(AnalyticsErrorEventSource.BUCKET_CREATION_NAME_STEP);
return;
@ -105,18 +151,104 @@ async function onCreate(): Promise<void> {
isLoading.value = true;
try {
await store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET, bucketName.value);
await store.dispatch(BUCKET_ACTIONS.FETCH, 1);
await store.dispatch(OBJECTS_ACTIONS.SET_FILE_COMPONENT_BUCKET_NAME, bucketName.value);
analytics.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
analytics.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
await router.push(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
closeModal();
} catch (error) {
await notify.error(`Unable to fetch buckets. ${error.message}`, AnalyticsErrorEventSource.BUCKET_CREATION_FLOW);
}
if (!promptForPassphrase.value) {
await store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET, bucketName.value);
await store.dispatch(BUCKET_ACTIONS.FETCH, 1);
await store.dispatch(OBJECTS_ACTIONS.SET_FILE_COMPONENT_BUCKET_NAME, bucketName.value);
analytics.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
analytics.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
await router.push(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
closeModal();
isLoading.value = false;
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
}
return;
}
if (gatewayCredentialsForCreate.value.accessKeyId) {
await store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET_WITH_NO_PASSPHRASE, bucketName.value);
await store.dispatch(BUCKET_ACTIONS.FETCH, 1);
analytics.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
closeModal();
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
}
return ;
}
if (!apiKey.value) {
await store.dispatch(ACCESS_GRANTS_ACTIONS.DELETE_BY_NAME_AND_PROJECT_ID, FILE_BROWSER_AG_NAME);
const cleanAPIKey: AccessGrant = await store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, FILE_BROWSER_AG_NAME);
await store.dispatch(OBJECTS_ACTIONS.SET_API_KEY, cleanAPIKey.secret);
}
const now = new Date();
const inOneHour = new Date(now.setHours(now.getHours() + 1));
await worker.value.postMessage({
'type': 'SetPermission',
'isDownload': false,
'isUpload': true,
'isList': false,
'isDelete': false,
'notAfter': inOneHour.toISOString(),
'buckets': [],
'apiKey': apiKey.value,
});
const grantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
if (grantEvent.data.error) {
await notify.error(grantEvent.data.error, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
return;
}
const salt = await store.dispatch(PROJECTS_ACTIONS.GET_SALT, store.getters.selectedProject.id);
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
worker.value.postMessage({
'type': 'GenerateAccess',
'apiKey': grantEvent.data.value,
'passphrase': '',
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const accessGrantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
if (accessGrantEvent.data.error) {
await notify.error(accessGrantEvent.data.error, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
return;
}
const accessGrant = accessGrantEvent.data.value;
const gatewayCredentials: EdgeCredentials = await store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, { accessGrant });
await store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS_FOR_CREATE, gatewayCredentials);
await store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET_WITH_NO_PASSPHRASE, bucketName.value);
await store.dispatch(BUCKET_ACTIONS.FETCH, 1);
analytics.eventTriggered(AnalyticsEvent.BUCKET_CREATED);
closeModal();
if (!bucketWasCreated.value) {
LocalData.setBucketWasCreatedStatus();
}
} catch (error) {
await notify.error(error.message, AnalyticsErrorEventSource.BUCKET_CREATION_FLOW);
} finally {
isLoading.value = false;
}
}
/**
@ -133,6 +265,18 @@ function closeModal(): void {
store.commit(APP_STATE_MUTATIONS.TOGGLE_CREATE_BUCKET_MODAL_SHOWN);
}
/**
* Sets local worker with worker instantiated in store.
*/
function setWorker(): void {
worker.value = store.state.accessGrantsModule.accessGrantsWebWorker;
if (worker.value) {
worker.value.onerror = (error: ErrorEvent) => {
notify.error(error.message, AnalyticsErrorEventSource.DELETE_BUCKET_MODAL);
};
}
}
/**
* Returns validation status of a bucket name.
*/
@ -150,6 +294,8 @@ function isBucketNameValid(name: string): boolean {
}
onMounted(async (): Promise<void> => {
setWorker();
try {
await store.dispatch(BUCKET_ACTIONS.FETCH_ALL_BUCKET_NAMES);
bucketName.value = allBucketNames.value.length > 0 ? '' : 'demo-bucket';

View File

@ -19,23 +19,11 @@
<EmptyBucketIcon class="buckets-table__no-buckets-area__image" />
<CreateBucketIcon class="buckets-table__no-buckets-area__small-image" />
<h4 class="buckets-table__no-buckets-area__title">There are no buckets in this project</h4>
<template v-if="promptForPassphrase">
<p class="buckets-table__no-buckets-area__body">Set an encryption passphrase to start uploading files.</p>
<VButton
label="Set Encryption Passphrase ->"
width="234px"
height="40px"
font-size="14px"
:on-press="onSetClick"
/>
</template>
<template v-else>
<p class="buckets-table__no-buckets-area__body">Create a new bucket to upload files</p>
<div class="new-bucket-button" :class="{ disabled: isLoading }" @click="onCreateBucketClick">
<WhitePlusIcon class="new-bucket-button__icon" />
<p class="new-bucket-button__label">New Bucket</p>
</div>
</template>
<p class="buckets-table__no-buckets-area__body">Create a new bucket to upload files</p>
<div class="new-bucket-button" :class="{ disabled: isLoading }" @click="onCreateBucketClick">
<WhitePlusIcon class="new-bucket-button__icon" />
<p class="new-bucket-button__label">New Bucket</p>
</div>
</div>
<div v-if="isNoSearchResultsShown" class="buckets-table__empty-search">

View File

@ -5,15 +5,7 @@
<div class="buckets-view">
<div class="buckets-view__title-area">
<h1 class="buckets-view__title-area__title" aria-roledescription="title">Buckets</h1>
<VButton
v-if="promptForPassphrase"
label="Set Encryption Passphrase ->"
width="234px"
height="40px"
font-size="14px"
:on-press="onSetClick"
/>
<div v-else class="buckets-view-button" :class="{ disabled: isLoading }" @click="onCreateBucketClick">
<div class="buckets-view-button" :class="{ disabled: isLoading }" @click="onCreateBucketClick">
<WhitePlusIcon class="buckets-view-button__icon" />
<p class="buckets-view-button__label">New Bucket</p>
</div>
@ -29,7 +21,6 @@
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { RouteConfig } from '@/router';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { LocalData } from '@/utils/localData';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
@ -40,7 +31,6 @@ import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import EncryptionBanner from '@/components/objects/EncryptionBanner.vue';
import BucketsTable from '@/components/objects/BucketsTable.vue';
import VButton from '@/components/common/VButton.vue';
import WhitePlusIcon from '@/../static/images/common/plusWhite.svg';
@ -50,7 +40,6 @@ import WhitePlusIcon from '@/../static/images/common/plusWhite.svg';
WhitePlusIcon,
BucketsTable,
EncryptionBanner,
VButton,
},
})
export default class BucketsView extends Vue {
@ -112,13 +101,6 @@ export default class BucketsView extends Vue {
}
}
/**
* Toggles create project passphrase modal visibility.
*/
public onSetClick(): void {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_CREATE_PROJECT_PASSPHRASE_MODAL_SHOWN);
}
/**
* Toggles create bucket modal visibility.
*/

View File

@ -10,7 +10,7 @@
height="100px"
/>
<template v-else>
<template v-if="promptForPassphrase">
<template v-if="promptForPassphrase && !bucketWasCreated">
<p class="dashboard-header__subtitle">
Set an encryption passphrase <br>to start uploading files.
</p>
@ -22,7 +22,7 @@
:on-press="onSetClick"
/>
</template>
<template v-else-if="!promptForPassphrase && !bucketsPage.buckets.length && !bucketsPage.search">
<template v-else-if="!promptForPassphrase && !bucketWasCreated && !bucketsPage.buckets.length && !bucketsPage.search">
<p class="dashboard-header__subtitle">
Create a bucket to start <br>uploading data in your project.
</p>
@ -67,6 +67,7 @@ import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
import { BucketPage } from '@/types/buckets';
import { ProjectLimits } from '@/types/projects';
import { RouteConfig } from '@/router';
import { LocalData } from '@/utils/localData';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
@ -87,6 +88,18 @@ const promptForPassphrase = computed((): boolean => {
return store.state.objectsModule.promptForPassphrase;
});
/**
* Indicates if bucket was created.
*/
const bucketWasCreated = computed((): boolean => {
const status = LocalData.getBucketWasCreatedStatus();
if (status !== null) {
return status;
}
return false;
});
/**
* Returns current limits from store.
*/

View File

@ -150,6 +150,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { computed } from 'vue';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
@ -235,6 +236,9 @@ export default class NewProjectDashboard extends Vue {
if (this.hasJustLoggedIn) {
if (this.limits.objectCount > 0) {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_ENTER_PASSPHRASE_MODAL_SHOWN);
if (!this.bucketWasCreated) {
LocalData.setBucketWasCreatedStatus();
}
} else {
this.$store.commit(APP_STATE_MUTATIONS.TOGGLE_CREATE_PROJECT_PASSPHRASE_MODAL_SHOWN);
}
@ -407,6 +411,18 @@ export default class NewProjectDashboard extends Vue {
return this.$store.state.appStateModule.appState.hasJustLoggedIn;
}
/**
* Indicates if bucket was created.
*/
private get bucketWasCreated(): boolean {
const status = LocalData.getBucketWasCreatedStatus();
if (status !== null) {
return status;
}
return false;
}
/**
* Formats value to needed form and returns it.
*/

View File

@ -12,12 +12,14 @@ export const OBJECTS_ACTIONS = {
CLEAR: 'clearObjects',
SET_GATEWAY_CREDENTIALS: 'setGatewayCredentials',
SET_GATEWAY_CREDENTIALS_FOR_DELETE: 'setGatewayCredentialsForDelete',
SET_GATEWAY_CREDENTIALS_FOR_CREATE: 'setGatewayCredentialsForCreate',
SET_API_KEY: 'setApiKey',
SET_S3_CLIENT: 'setS3Client',
SET_PASSPHRASE: 'setPassphrase',
SET_FILE_COMPONENT_BUCKET_NAME: 'setFileComponentBucketName',
FETCH_BUCKETS: 'fetchBuckets',
CREATE_BUCKET: 'createBucket',
CREATE_BUCKET_WITH_NO_PASSPHRASE: 'createBucketWithNoPassphrase',
DELETE_BUCKET: 'deleteBucket',
CHECK_ONGOING_UPLOADS: 'checkOngoingUploads',
};
@ -25,10 +27,12 @@ export const OBJECTS_ACTIONS = {
export const OBJECTS_MUTATIONS = {
SET_GATEWAY_CREDENTIALS: 'SET_GATEWAY_CREDENTIALS',
SET_GATEWAY_CREDENTIALS_FOR_DELETE: 'SET_GATEWAY_CREDENTIALS_FOR_DELETE',
SET_GATEWAY_CREDENTIALS_FOR_CREATE: 'SET_GATEWAY_CREDENTIALS_FOR_CREATE',
SET_API_KEY: 'SET_API_KEY',
CLEAR: 'CLEAR_OBJECTS',
SET_S3_CLIENT: 'SET_S3_CLIENT',
SET_S3_CLIENT_FOR_DELETE: 'SET_S3_CLIENT_FOR_DELETE',
SET_S3_CLIENT_FOR_CREATE: 'SET_S3_CLIENT_FOR_CREATE',
SET_BUCKETS: 'SET_BUCKETS',
SET_FILE_COMPONENT_BUCKET_NAME: 'SET_FILE_COMPONENT_BUCKET_NAME',
SET_PASSPHRASE: 'SET_PASSPHRASE',
@ -41,8 +45,10 @@ const {
SET_API_KEY,
SET_GATEWAY_CREDENTIALS,
SET_GATEWAY_CREDENTIALS_FOR_DELETE,
SET_GATEWAY_CREDENTIALS_FOR_CREATE,
SET_S3_CLIENT,
SET_S3_CLIENT_FOR_DELETE,
SET_S3_CLIENT_FOR_CREATE,
SET_BUCKETS,
SET_PASSPHRASE,
SET_PROMPT_FOR_PASSPHRASE,
@ -54,6 +60,7 @@ export class ObjectsState {
public apiKey = '';
public gatewayCredentials: EdgeCredentials = new EdgeCredentials();
public gatewayCredentialsForDelete: EdgeCredentials = new EdgeCredentials();
public gatewayCredentialsForCreate: EdgeCredentials = new EdgeCredentials();
public s3Client: S3 = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
@ -64,6 +71,11 @@ export class ObjectsState {
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
});
public s3ClientForCreate: S3 = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
});
public bucketsList: Bucket[] = [];
public passphrase = '';
public promptForPassphrase = true;
@ -96,6 +108,9 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
[SET_GATEWAY_CREDENTIALS_FOR_DELETE](state: ObjectsState, credentials: EdgeCredentials) {
state.gatewayCredentialsForDelete = credentials;
},
[SET_GATEWAY_CREDENTIALS_FOR_CREATE](state: ObjectsState, credentials: EdgeCredentials) {
state.gatewayCredentialsForCreate = credentials;
},
[SET_S3_CLIENT](state: ObjectsState) {
const s3Config = {
accessKeyId: state.gatewayCredentials.accessKeyId,
@ -120,6 +135,18 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
state.s3ClientForDelete = new S3(s3Config);
},
[SET_S3_CLIENT_FOR_CREATE](state: ObjectsState) {
const s3Config = {
accessKeyId: state.gatewayCredentialsForCreate.accessKeyId,
secretAccessKey: state.gatewayCredentialsForCreate.secretKey,
endpoint: state.gatewayCredentialsForCreate.endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
};
state.s3ClientForCreate = new S3(s3Config);
},
[SET_BUCKETS](state: ObjectsState, buckets: Bucket[]) {
state.bucketsList = buckets;
},
@ -141,6 +168,7 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
state.promptForPassphrase = true;
state.gatewayCredentials = new EdgeCredentials();
state.gatewayCredentialsForDelete = new EdgeCredentials();
state.gatewayCredentialsForCreate = new EdgeCredentials();
state.s3Client = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
@ -151,6 +179,11 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
});
state.s3ClientForCreate = new S3({
s3ForcePathStyle: true,
signatureVersion: 'v4',
httpOptions: { timeout: 0 },
});
state.bucketsList = [];
state.fileComponentBucketName = '';
state.leaveRoute = '';
@ -167,6 +200,10 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
commit(SET_GATEWAY_CREDENTIALS_FOR_DELETE, credentials);
commit(SET_S3_CLIENT_FOR_DELETE);
},
setGatewayCredentialsForCreate: function({ commit }: ObjectsContext, credentials: EdgeCredentials): void {
commit(SET_GATEWAY_CREDENTIALS_FOR_CREATE, credentials);
commit(SET_S3_CLIENT_FOR_CREATE);
},
setS3Client: function({ commit }: ObjectsContext): void {
commit(SET_S3_CLIENT);
},
@ -186,6 +223,11 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
Bucket: name,
}).promise();
},
createBucketWithNoPassphrase: async function(ctx, name: string): Promise<void> {
await ctx.state.s3ClientForCreate.createBucket({
Bucket: name,
}).promise();
},
deleteBucket: async function(ctx, name: string): Promise<void> {
await ctx.state.s3ClientForDelete.deleteBucket({
Bucket: name,

View File

@ -6,6 +6,7 @@
*/
export class LocalData {
private static selectedProjectId = 'selectedProjectId';
private static bucketWasCreated = 'bucketWasCreated';
private static demoBucketCreated = 'demoBucketCreated';
private static bucketGuideHidden = 'bucketGuideHidden';
private static serverSideEncryptionBannerHidden = 'serverSideEncryptionBannerHidden';
@ -36,6 +37,17 @@ export class LocalData {
localStorage.setItem(LocalData.demoBucketCreated, 'true');
}
public static setBucketWasCreatedStatus(): void {
localStorage.setItem(LocalData.bucketWasCreated, 'true');
}
public static getBucketWasCreatedStatus(): boolean | null {
const status = localStorage.getItem(LocalData.bucketWasCreated);
if (!status) return null;
return JSON.parse(status);
}
/**
* "Disable" showing the upload guide tooltip on the bucket page
*/