web/satellite/vuetify-poc: add open bucket passphrase

This change requires a passphrase before opening a bucket and after
opening the objects page without going through the buckets page. It also shows if there are
objects stored with different passphrase.

Issues: https://github.com/storj/storj/issues/6100
https://github.com/storj/storj/issues/6110

Change-Id: Ica509fd6db968ef9c232a05b3ee39e0ab5f746e1
This commit is contained in:
Wilfred Asomani 2023-08-17 12:09:21 +00:00 committed by Storj Robot
parent 2b278bb05f
commit f46280c93b
3 changed files with 299 additions and 17 deletions

View File

@ -91,6 +91,7 @@
</v-data-table-server>
</v-card>
<delete-bucket-dialog v-model="isDeleteBucketDialogShown" :bucket-name="bucketToDelete" />
<enter-bucket-passphrase-dialog v-model="isBucketPassphraseDialogOpen" @passphraseEntered="() => openBucket(selectedBucketName)" />
</template>
<script setup lang="ts">
@ -106,10 +107,15 @@ import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames
import { useProjectsStore } from '@/store/modules/projectsStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
import { tableSizeOptions } from '@/types/common';
import { RouteConfig } from '@/types/router';
import { EdgeCredentials } from '@/types/accessGrants';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import IconTrash from '@poc/components/icons/IconTrash.vue';
import DeleteBucketDialog from '@poc/components/dialogs/DeleteBucketDialog.vue';
import EnterBucketPassphraseDialog from '@poc/components/dialogs/EnterBucketPassphraseDialog.vue';
const analyticsStore = useAnalyticsStore();
const bucketsStore = useBucketsStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
@ -122,6 +128,7 @@ const searchTimer = ref<NodeJS.Timeout>();
const selected = ref([]);
const isDeleteBucketDialogShown = ref<boolean>(false);
const bucketToDelete = ref<string>('');
const isBucketPassphraseDialogOpen = ref(false);
const headers = [
{
@ -152,6 +159,27 @@ const page = computed((): BucketPage => {
return bucketsStore.state.page;
});
/**
* Returns condition if user has to be prompt for passphrase from store.
*/
const promptForPassphrase = computed((): boolean => {
return bucketsStore.state.promptForPassphrase;
});
/**
* Returns selected bucket's name from store.
*/
const selectedBucketName = computed((): string => {
return bucketsStore.state.fileComponentBucketName;
});
/**
* Returns edge credentials from store.
*/
const edgeCredentials = computed((): EdgeCredentials => {
return bucketsStore.state.edgeCredentials;
});
/**
* Fetches bucket using api.
*/
@ -178,6 +206,39 @@ function onUpdatePage(page: number): void {
fetchBuckets(page, cursor.value.limit);
}
/**
* Navigates to bucket page.
*/
async function openBucket(bucketName: string): Promise<void> {
if (!bucketName) {
return;
}
bucketsStore.setFileComponentBucketName(bucketName);
if (!promptForPassphrase.value) {
if (!edgeCredentials.value.accessKeyId) {
try {
await bucketsStore.setS3Client(projectsStore.state.selectedProject.id);
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.BUCKET_TABLE);
return;
}
}
analyticsStore.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path);
router.push(`/projects/${projectsStore.state.selectedProject.id}/buckets/${bucketsStore.state.fileComponentBucketName}`);
return;
}
isBucketPassphraseDialogOpen.value = true;
}
/**
* Displays the Delete Bucket dialog.
*/
function showDeleteBucketDialog(bucketName: string): void {
bucketToDelete.value = bucketName;
isDeleteBucketDialogShown.value = true;
}
/**
* Handles update table search.
*/
@ -190,21 +251,6 @@ watch(() => search.value, () => {
}, 500); // 500ms delay for every new call.
});
/**
* Navigates to bucket page.
*/
function openBucket(bucketName: string): void {
router.push(`/projects/${projectsStore.state.selectedProject.id}/buckets/${bucketName}`);
}
/**
* Displays the Delete Bucket dialog.
*/
function showDeleteBucketDialog(bucketName: string): void {
bucketToDelete.value = bucketName;
isDeleteBucketDialogShown.value = true;
}
onMounted(() => {
fetchBuckets();
});

View File

@ -0,0 +1,207 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="400px"
transition="fade-transition"
>
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="py-4 pl-7">
<template #prepend>
<img class="d-block" src="@/../static/images/accessGrants/newCreateFlow/accessEncryption.svg">
</template>
<v-card-title class="font-weight-bold">
Enter passphrase
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
<v-divider />
<v-card-item class="py-4">
<v-form v-model="formValid">
<v-row>
<v-col cols="12">
<p>
Enter your encryption passphrase to view and manage your data in the browser.
This passphrase will be used to unlock all buckets in this project.
</p>
</v-col>
<v-col cols="12">
<v-text-field
v-model="passphrase"
:base-color="isWarningState ? 'warning' : ''"
label="Encryption Passphrase"
:append-inner-icon="isPassphraseVisible ? 'mdi-eye-off' : 'mdi-eye'"
:type="isPassphraseVisible ? 'text' : 'password'"
variant="outlined"
hide-details="auto"
:rules="[ RequiredRule ]"
@click:append-inner="isPassphraseVisible = !isPassphraseVisible"
/>
</v-col>
</v-row>
</v-form>
<v-alert
v-if="isWarningState"
class="mt-3"
density="compact"
type="warning"
text="This bucket includes files that are uploaded using a different encryption passphrase from the one you entered."
/>
</v-card-item>
<v-divider />
<v-card-actions>
<v-col>
<v-btn
variant="outlined"
color="default"
block
:disabled="isLoading"
@click="model = false"
>
Cancel
</v-btn>
</v-col>
<v-col>
<v-btn
:color="isWarningState ? 'default' : 'primary'"
:variant="isWarningState ? 'outlined' : 'flat'"
block
:loading="isLoading"
:disabled="!formValid"
@click="onContinue"
>
Continue ->
</v-btn>
</v-col>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { Component, computed, ref, watch } from 'vue';
import { VForm, VRow, VCol, VTextField, VCardItem, VDivider, VCardTitle, VBtn, VCard, VCardActions, VDialog, VAlert } from 'vuetify/components';
import { RequiredRule } from '@poc/types/common';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useAppStore } from '@/store/modules/appStore';
import { Bucket } from '@/types/buckets';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useLoading } from '@/composables/useLoading';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
const analyticsStore = useAnalyticsStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const NUMBER_OF_DISPLAYED_OBJECTS = 1000;
const passphrase = ref<string>('');
const isPassphraseVisible = ref<boolean>(false);
const isWarningState = ref<boolean>(false);
const innerContent = ref<Component | null>(null);
const formValid = ref<boolean>(false);
const props = defineProps<{
modelValue: boolean,
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void,
(event: 'passphraseEntered'): void,
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
/**
* Returns chosen bucket name from store.
*/
const bucketName = computed((): string => {
return bucketsStore.state.fileComponentBucketName;
});
/**
* Returns selected bucket name object count.
*/
const bucketObjectCount = computed((): number => {
const data: Bucket | undefined = bucketsStore.state.page.buckets.find(
(bucket: Bucket) => bucket.name === bucketName.value,
);
return data?.objectCount || 0;
});
/**
* Sets access and navigates to object browser.
*/
async function onContinue(): Promise<void> {
if (isLoading.value) return;
if (isWarningState.value) {
bucketsStore.setPromptForPassphrase(false);
model.value = false;
emit('passphraseEntered');
return;
}
isLoading.value = true;
try {
bucketsStore.setPassphrase(passphrase.value);
await bucketsStore.setS3Client(projectsStore.state.selectedProject.id);
const count: number = await bucketsStore.getObjectsCount(bucketName.value);
if (bucketObjectCount.value > count && bucketObjectCount.value <= NUMBER_OF_DISPLAYED_OBJECTS) {
isWarningState.value = true;
isLoading.value = false;
return;
}
bucketsStore.setPromptForPassphrase(false);
isLoading.value = false;
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.OPEN_BUCKET_MODAL);
isLoading.value = false;
return;
}
model.value = false;
emit('passphraseEntered');
}
watch(innerContent, comp => {
if (!comp) {
passphrase.value = '';
isWarningState.value = false;
return;
}
});
</script>

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
<template>
<v-container>
<v-container v-if="hasObjects">
<PageTitleComponent title="Browse Files" />
<BrowserBreadcrumbsComponent />
@ -33,11 +33,16 @@
<BrowserTableComponent />
</v-container>
<EnterBucketPassphraseDialog v-model="isBucketPassphraseDialogOpen" @passphraseEntered="getObjects" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { VContainer, VCol, VRow, VBtn } from 'vuetify/components';
import { useRoute } from 'vue-router';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import BrowserBreadcrumbsComponent from '@poc/components/BrowserBreadcrumbsComponent.vue';
@ -46,6 +51,30 @@ import BrowserTableComponent from '@poc/components/BrowserTableComponent.vue';
import BrowserNewFolderDialog from '@poc/components/dialogs/BrowserNewFolderDialog.vue';
import IconUpload from '@poc/components/icons/IconUpload.vue';
import IconFolder from '@poc/components/icons/IconFolder.vue';
import EnterBucketPassphraseDialog from '@poc/components/dialogs/EnterBucketPassphraseDialog.vue';
const bucketsStore = useBucketsStore();
const route = useRoute();
const snackbar = ref<boolean>(false);
const isBucketPassphraseDialogOpen = ref(false);
const hasObjects = ref(false);
const bucketName = computed(() => {
return bucketsStore.state.fileComponentBucketName;
});
function getObjects() {
hasObjects.value = true;
}
onMounted(() => {
if (!bucketName.value) {
// navigated here via direct link or reloaded this page
bucketsStore.setFileComponentBucketName(route.params['bucketName'] as string);
isBucketPassphraseDialogOpen.value = true;
return;
}
getObjects();
});
</script>