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:
parent
2b278bb05f
commit
f46280c93b
@ -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();
|
||||
});
|
||||
|
@ -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>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user