web/satellite/vuetify-poc: add access grant creation flow

This change implements the access grant creation flow in the Vuetify
project.

Resolves #6061

Change-Id: I233088cbfcfe458936410899531389e290f276d4
This commit is contained in:
Jeremy Wharton 2023-07-28 09:29:32 -05:00 committed by Storj Robot
parent dc41978743
commit ea94bc7f6d
22 changed files with 1915 additions and 186 deletions

View File

@ -80,6 +80,7 @@ module.exports = {
'vue/no-unused-refs': ['warn'],
'vue/no-unused-vars': ['warn'],
'vue/no-useless-v-bind': ['warn'],
'vue/no-v-model-argument': ['off'],
'vue/valid-v-slot': ['error', { 'allowModifiers': true }],
'vue/no-useless-template-attributes': ['off'], // TODO: fix later

View File

@ -27,6 +27,7 @@ export default defineConfig({
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@poc': fileURLToPath(new URL('./vuetify-poc/src', import.meta.url)),
'stream': 'stream-browserify', // Passphrase mnemonic generation will not work without this
},
extensions: [
'.js',

View File

@ -0,0 +1,614 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="400px"
transition="fade-transition"
scrollable
:persistent="isCreating"
>
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="pl-7 py-4 create-access-dialog__header">
<template #prepend>
<img class="d-block" :src="STEP_ICON_AND_TITLE[step].icon">
</template>
<v-card-title class="font-weight-bold">
{{ stepInfos[step].ref.value?.title }}
</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isCreating"
@click="model = false"
/>
</template>
<v-progress-linear height="2px" indeterminate absolute location="bottom" :active="isFetching || isCreating" />
</v-card-item>
<v-divider />
<v-window
v-model="step"
class="overflow-y-auto create-access-dialog__window"
:class="{ 'create-access-dialog__window--loading': isFetching }"
>
<v-window-item :value="CreateAccessStep.CreateNewAccess">
<create-new-access-step
:ref="stepInfos[CreateAccessStep.CreateNewAccess].ref"
@name-changed="newName => name = newName"
@types-changed="newTypes => accessTypes = newTypes"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.EncryptionInfo">
<encryption-info-step :ref="stepInfos[CreateAccessStep.EncryptionInfo].ref" />
</v-window-item>
<v-window-item :value="CreateAccessStep.ChoosePermission">
<choose-permissions-step
:ref="stepInfos[CreateAccessStep.ChoosePermission].ref"
@buckets-changed="newBuckets => buckets = newBuckets"
@permissions-changed="newPerms => permissions = newPerms"
@end-date-changed="newDate => endDate = newDate"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.AccessEncryption">
<access-encryption-step
:ref="stepInfos[CreateAccessStep.AccessEncryption].ref"
@select-option="newOpt => passphraseOption = newOpt"
@passphrase-changed="newPass => passphrase = newPass"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.EnterMyPassphrase">
<enter-passphrase-step
:ref="stepInfos[CreateAccessStep.EnterMyPassphrase].ref"
:passphrase-type="CreateAccessStep.EnterMyPassphrase"
@passphrase-changed="newPass => passphrase = newPass"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.EnterNewPassphrase">
<enter-passphrase-step
:ref="stepInfos[CreateAccessStep.EnterNewPassphrase].ref"
:passphrase-type="CreateAccessStep.EnterNewPassphrase"
@passphrase-changed="newPass => passphrase = newPass"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.PassphraseGenerated">
<passphrase-generated-step
:ref="stepInfos[CreateAccessStep.PassphraseGenerated].ref"
:name="name"
@passphrase-changed="newPass => passphrase = newPass"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.ConfirmDetails">
<confirm-details-step
:ref="stepInfos[CreateAccessStep.ConfirmDetails].ref"
:name="name"
:types="accessTypes"
:permissions="permissions"
:buckets="buckets"
:end-date="(endDate as AccessGrantEndDate)"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.AccessCreated">
<access-created-step
:ref="stepInfos[CreateAccessStep.AccessCreated].ref"
:name="name"
:access-grant="accessGrant"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.CLIAccessCreated">
<c-l-i-access-created-step
:ref="stepInfos[CreateAccessStep.CLIAccessCreated].ref"
:name="name"
:api-key="cliAccess"
/>
</v-window-item>
<v-window-item :value="CreateAccessStep.CredentialsCreated">
<s3-credentials-created-step
:ref="stepInfos[CreateAccessStep.CredentialsCreated].ref"
:name="name"
:access-key="edgeCredentials.accessKeyId"
:secret-key="edgeCredentials.secretKey"
:endpoint="edgeCredentials.endpoint"
/>
</v-window-item>
</v-window>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn
v-bind="stepInfos[step].prev.value ? undefined : {
'href': stepInfos[step].docsLink || 'https://docs.storj.io/dcs/access',
'target': '_blank',
'rel': 'noopener noreferrer',
}"
variant="outlined"
color="default"
block
:prepend-icon="stepInfos[step].prev.value ? 'mdi-chevron-left' : 'mdi-book-open-outline'"
:disabled="isCreating || isFetching"
@click="prevStep"
>
{{ stepInfos[step].prev.value ? 'Back' : 'Learn More' }}
</v-btn>
</v-col>
<v-col>
<v-btn
color="primary"
variant="flat"
block
:append-icon="stepInfos[step].next.value ? 'mdi-chevron-right' : undefined"
:loading="isCreating"
:disabled="isFetching"
@click="nextStep"
>
{{ stepInfos[step].nextText.value }}
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { Component, Ref, computed, ref, watch, WatchStopHandle, onMounted } from 'vue';
import {
VCol,
VRow,
VBtn,
VDialog,
VCard,
VSheet,
VCardItem,
VCardTitle,
VDivider,
VWindow,
VWindowItem,
VCardActions,
VProgressLinear,
} from 'vuetify/components';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useNotify } from '@/utils/hooks';
import { AccessType, PassphraseOption, Permission, CreateAccessStep, STEP_ICON_AND_TITLE } from '@/types/createAccessGrant';
import { AccessGrantEndDate, ACCESS_TYPE_LINKS, CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import { LocalData } from '@/utils/localData';
import { AccessGrant, EdgeCredentials } from '@/types/accessGrants';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import CreateNewAccessStep from '@poc/components/dialogs/createAccessSteps/CreateNewAccessStep.vue';
import ChoosePermissionsStep from '@poc/components/dialogs/createAccessSteps/ChoosePermissionsStep.vue';
import AccessEncryptionStep from '@poc/components/dialogs/createAccessSteps/AccessEncryptionStep.vue';
import EncryptionInfoStep from '@poc/components/dialogs/createAccessSteps/EncryptionInfoStep.vue';
import EnterPassphraseStep from '@poc/components/dialogs/createAccessSteps/EnterPassphraseStep.vue';
import PassphraseGeneratedStep from '@poc/components/dialogs/createAccessSteps/PassphraseGeneratedStep.vue';
import ConfirmDetailsStep from '@poc/components/dialogs/createAccessSteps/ConfirmDetailsStep.vue';
import AccessCreatedStep from '@poc/components/dialogs/createAccessSteps/AccessCreatedStep.vue';
import CLIAccessCreatedStep from '@poc/components/dialogs/createAccessSteps/CLIAccessCreatedStep.vue';
import S3CredentialsCreatedStep from '@poc/components/dialogs/createAccessSteps/S3CredentialsCreatedStep.vue';
type CreateAccessLocation = CreateAccessStep | null | (() => (CreateAccessStep | null));
class StepInfo {
public ref: Ref<CreateAccessStepComponent | null> = ref<CreateAccessStepComponent | null>(null);
public prev: Ref<CreateAccessStep | null>;
public next: Ref<CreateAccessStep | null>;
public nextText: Ref<string>;
constructor(
prev: CreateAccessLocation = null,
next: CreateAccessLocation = null,
public docsLink: string | null = null,
nextText: string | (() => string) = 'Next',
public beforeNext?: () => Promise<boolean>,
) {
this.prev = (typeof prev === 'function') ? computed<CreateAccessStep | null>(prev) : ref<CreateAccessStep | null>(prev);
this.next = (typeof next === 'function') ? computed<CreateAccessStep | null>(next) : ref<CreateAccessStep | null>(next);
this.nextText = (typeof nextText === 'function') ? computed<string>(nextText) : ref<string>(nextText);
}
}
const resets: (() => void)[] = [];
function resettableRef<T>(value: T): Ref<T> {
const thisRef = ref<T>(value) as Ref<T>;
resets.push(() => thisRef.value = value);
return thisRef;
}
const props = defineProps<{
modelValue: boolean,
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => {
if (isCreating.value) return;
emit('update:modelValue', value);
},
});
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const bucketsStore = useBucketsStore();
const projectsStore = useProjectsStore();
const agStore = useAccessGrantsStore();
const configStore = useConfigStore();
const notify = useNotify();
const analyticsStore = useAnalyticsStore();
const innerContent = ref<Component | null>(null);
const step = resettableRef<CreateAccessStep>(CreateAccessStep.CreateNewAccess);
const isFetching = ref<boolean>(true);
// Create New Access
const name = resettableRef<string>('');
const accessTypes = resettableRef<AccessType[]>([]);
// Permissions
const permissions = resettableRef<Permission[]>([]);
const buckets = resettableRef<string[]>([]);
const endDate = resettableRef<AccessGrantEndDate | null>(null);
// Select Passphrase Type
const passphraseOption = resettableRef<PassphraseOption | null>(null);
// Enter / Generate Passphrase
const passphrase = resettableRef<string>('');
// Confirm Details
const isCreating = ref<boolean>(false);
// Access Created
const accessGrant = resettableRef<string>('');
// S3 Credentials Created
const edgeCredentials = resettableRef<EdgeCredentials>(new EdgeCredentials());
// CLI Access Created
const cliAccess = resettableRef<string>('');
const worker = ref<Worker | null>(null);
const stepInfos: Record<CreateAccessStep, StepInfo> = {
[CreateAccessStep.CreateNewAccess]: new StepInfo(
null,
() => (accessTypes.value.includes(AccessType.S3) && !LocalData.getServerSideEncryptionModalHidden())
? CreateAccessStep.EncryptionInfo
: CreateAccessStep.ChoosePermission,
),
[CreateAccessStep.EncryptionInfo]: new StepInfo(
CreateAccessStep.CreateNewAccess,
CreateAccessStep.ChoosePermission,
),
[CreateAccessStep.ChoosePermission]: new StepInfo(
() => (accessTypes.value.includes(AccessType.S3) && !LocalData.getServerSideEncryptionModalHidden())
? CreateAccessStep.EncryptionInfo
: CreateAccessStep.CreateNewAccess,
() => accessTypes.value.includes(AccessType.APIKey) ? CreateAccessStep.ConfirmDetails : CreateAccessStep.AccessEncryption,
),
[CreateAccessStep.AccessEncryption]: new StepInfo(
CreateAccessStep.ChoosePermission,
() => {
switch (passphraseOption.value) {
case PassphraseOption.EnterNewPassphrase: return CreateAccessStep.EnterNewPassphrase;
case PassphraseOption.GenerateNewPassphrase: return CreateAccessStep.PassphraseGenerated;
case PassphraseOption.SetMyProjectPassphrase: return CreateAccessStep.EnterMyPassphrase;
default: return CreateAccessStep.ConfirmDetails;
}
},
),
[CreateAccessStep.EnterMyPassphrase]: new StepInfo(
CreateAccessStep.AccessEncryption,
CreateAccessStep.ConfirmDetails,
),
[CreateAccessStep.EnterNewPassphrase]: new StepInfo(
CreateAccessStep.AccessEncryption,
CreateAccessStep.ConfirmDetails,
),
[CreateAccessStep.PassphraseGenerated]: new StepInfo(
CreateAccessStep.AccessEncryption,
CreateAccessStep.ConfirmDetails,
),
[CreateAccessStep.ConfirmDetails]: new StepInfo(
() => {
switch (passphraseOption.value) {
case PassphraseOption.EnterNewPassphrase: return CreateAccessStep.EnterNewPassphrase;
case PassphraseOption.GenerateNewPassphrase: return CreateAccessStep.PassphraseGenerated;
case PassphraseOption.SetMyProjectPassphrase: return CreateAccessStep.EnterMyPassphrase;
default: return CreateAccessStep.AccessEncryption;
}
},
() => {
if (accessTypes.value.includes(AccessType.AccessGrant)) return CreateAccessStep.AccessCreated;
if (accessTypes.value.includes(AccessType.S3)) return CreateAccessStep.CredentialsCreated;
return CreateAccessStep.CLIAccessCreated;
},
null,
'Create Access',
async () => {
isCreating.value = true;
try {
await createCLIAccess();
if (accessTypes.value.includes(AccessType.AccessGrant) || accessTypes.value.includes(AccessType.S3)) {
await createAccessGrant();
}
if (accessTypes.value.includes(AccessType.S3)) {
await createEdgeCredentials();
}
} catch (error) {
notify.error(`Error creating access grant. ${error.message}`, AnalyticsErrorEventSource.CREATE_AG_MODAL);
isCreating.value = false;
return false;
}
// This is an action to handle case if user sets project level passphrase.
if (
passphraseOption.value === PassphraseOption.SetMyProjectPassphrase &&
!accessTypes.value.includes(AccessType.APIKey)
) {
bucketsStore.setEdgeCredentials(new EdgeCredentials());
bucketsStore.setPassphrase(passphrase.value);
bucketsStore.setPromptForPassphrase(false);
}
isCreating.value = false;
return true;
},
),
[CreateAccessStep.AccessCreated]: new StepInfo(
null,
() => (accessTypes.value.includes(AccessType.S3)) ? CreateAccessStep.CredentialsCreated : null,
ACCESS_TYPE_LINKS[AccessType.AccessGrant],
() => (accessTypes.value.includes(AccessType.S3)) ? 'Next' : 'Finish',
),
[CreateAccessStep.CredentialsCreated]: new StepInfo(null, null, ACCESS_TYPE_LINKS[AccessType.S3], 'Finish'),
[CreateAccessStep.CLIAccessCreated]: new StepInfo(null, null, ACCESS_TYPE_LINKS[AccessType.APIKey], 'Finish'),
};
/**
* Navigates to the next step.
*/
async function nextStep(): Promise<void> {
const info = stepInfos[step.value];
if (isCreating.value || isFetching.value || info.ref.value?.validate?.() === false) return;
if (info.beforeNext) try {
if (!await info.beforeNext()) return;
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.CREATE_AG_MODAL);
return;
}
const next = info.next.value;
if (!next) {
info.ref.value?.onExit?.();
model.value = false;
return;
}
step.value = next;
}
/**
* Navigates to the previous step.
*/
function prevStep(): void {
const prev = stepInfos[step.value].prev.value;
if (!prev) return;
step.value = prev;
}
/**
* Initializes the current step when it has changed.
*/
watch(step, (newStep, oldStep) => {
if (!innerContent.value) return;
stepInfos[oldStep].ref.value?.onExit?.();
// Window items are lazy loaded, so the component may not exist yet
let unwatch: WatchStopHandle | null = null;
let unwatchImmediately = false;
unwatch = watch(
() => stepInfos[newStep].ref.value,
stepComp => {
if (!stepComp) return;
stepComp.onEnter?.();
if (unwatch) {
unwatch();
return;
}
unwatchImmediately = true;
},
{ immediate: true },
);
if (unwatchImmediately) unwatch();
});
/**
* Generates CLI access.
*/
async function createCLIAccess(): Promise<void> {
if (!worker.value) {
throw new Error('Web worker is not initialized.');
}
const projectID = projectsStore.state.selectedProject.id;
// creates fresh new API key.
const cleanAPIKey: AccessGrant = await agStore.createAccessGrant(name.value, projectID);
await agStore.getAccessGrants(1, projectID).catch(err => {
notify.error(`Unable to fetch access grants. ${err.message}`, AnalyticsErrorEventSource.CREATE_AG_MODAL);
});
let permissionsMsg = {
'type': 'SetPermission',
'buckets': JSON.stringify(buckets.value),
'apiKey': cleanAPIKey.secret,
'isDownload': permissions.value.includes(Permission.Read),
'isUpload': permissions.value.includes(Permission.Write),
'isList': permissions.value.includes(Permission.List),
'isDelete': permissions.value.includes(Permission.Delete),
'notBefore': new Date().toISOString(),
};
const notAfter = endDate.value?.date;
if (notAfter) permissionsMsg = Object.assign(permissionsMsg, { 'notAfter': notAfter.toISOString() });
await worker.value.postMessage(permissionsMsg);
const grantEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
if (grantEvent.data.error) {
throw new Error(grantEvent.data.error);
}
cliAccess.value = grantEvent.data.value;
if (accessTypes.value.includes(AccessType.APIKey)) {
analyticsStore.eventTriggered(AnalyticsEvent.API_ACCESS_CREATED);
}
}
/**
* Generates access grant.
*/
async function createAccessGrant(): Promise<void> {
if (!worker.value) {
throw new Error('Web worker is not initialized.');
}
// creates access credentials.
const satelliteNodeURL = configStore.state.config.satelliteNodeURL;
const salt = await projectsStore.getProjectSalt(projectsStore.state.selectedProject.id);
if (!passphrase.value) {
throw new Error('Passphrase can\'t be empty');
}
worker.value.postMessage({
'type': 'GenerateAccess',
'apiKey': cliAccess.value,
'passphrase': passphrase.value,
'salt': salt,
'satelliteNodeURL': satelliteNodeURL,
});
const accessEvent: MessageEvent = await new Promise(resolve => {
if (worker.value) {
worker.value.onmessage = resolve;
}
});
if (accessEvent.data.error) {
throw new Error(accessEvent.data.error);
}
accessGrant.value = accessEvent.data.value;
if (accessTypes.value.includes(AccessType.AccessGrant)) {
analyticsStore.eventTriggered(AnalyticsEvent.ACCESS_GRANT_CREATED);
}
}
/**
* Generates edge credentials.
*/
async function createEdgeCredentials(): Promise<void> {
edgeCredentials.value = await agStore.getEdgeCredentials(accessGrant.value);
analyticsStore.eventTriggered(AnalyticsEvent.GATEWAY_CREDENTIALS_CREATED);
}
/**
* Executes when the dialog's inner content has been added or removed.
* If removed, refs are reset back to their initial values.
* Otherwise, data is fetched and the current step is initialized.
*
* This is used instead of onMounted because the dialog remains mounted
* even when hidden.
*/
watch(innerContent, async (comp: Component): Promise<void> => {
if (!comp) {
resets.forEach(reset => reset());
return;
}
isFetching.value = true;
const projectID = projectsStore.state.selectedProject.id;
await agStore.getAllAGNames(projectID).catch(err => {
notify.error(`Error fetching access grant names. ${err.message}`, AnalyticsErrorEventSource.CREATE_AG_MODAL);
});
await bucketsStore.getAllBucketsNames(projectID).catch(err => {
notify.error(`Error fetching bucket grant names. ${err.message}`, AnalyticsErrorEventSource.CREATE_AG_MODAL);
});
isFetching.value = false;
stepInfos[step.value].ref.value?.onEnter?.();
});
onMounted(() => {
worker.value = agStore.state.accessGrantsWebWorker;
if (!worker.value) return;
worker.value.onerror = (error: ErrorEvent) => {
notify.error(error.message, AnalyticsErrorEventSource.CREATE_AG_MODAL);
};
});
</script>
<style scoped lang="scss">
.create-access-dialog {
&__header {
position: relative;
}
&__window {
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
&--loading {
opacity: 0.3;
transition: opacity 0s;
pointer-events: none;
}
}
}
</style>

View File

@ -0,0 +1,42 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pa-8">
<v-row>
<v-col cols="12">
Copy or save the Access Grant as it will only appear once.
</v-col>
<save-buttons :items="[ accessGrant ]" :access-name="name" file-name-base="access" />
<v-divider class="my-3" />
<v-col cols="12">
<text-output-area ref="output" label="Access Grant" :value="accessGrant" :tooltip-disabled="isTooltipDisabled" />
</v-col>
</v-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { VRow, VCol, VDivider } from 'vuetify/components';
import { CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import TextOutputArea from '@poc/components/dialogs/createAccessSteps/TextOutputArea.vue';
import SaveButtons from '@poc/components/dialogs/createAccessSteps/SaveButtons.vue';
const props = defineProps<{
name: string;
accessGrant: string;
}>();
const output = ref<InstanceType<typeof TextOutputArea> | null>(null);
const isTooltipDisabled = ref<boolean>(false);
defineExpose<CreateAccessStepComponent>({
title: 'Access Created',
onEnter: () => isTooltipDisabled.value = false,
onExit: () => isTooltipDisabled.value = true,
});
</script>

View File

@ -0,0 +1,133 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-form ref="form" class="pa-8">
<v-row>
<v-col cols="12">
<p class="text-subtitle-2 font-weight-bold mb-2">Encryption Passphrase</p>
<v-radio-group v-model="passphraseOption" :rules="[ RequiredRule ]" hide-details="auto">
<v-radio v-if="isPromptForPassphrase" label="Enter your project passphrase" :value="PassphraseOption.SetMyProjectPassphrase">
<template #label>
Enter your project passphrase
<info-tooltip>
You will enter your encryption passphrase on the next step.
Make sure it's the same one you use for this project.
This will allow you to manage existing data you have uploaded with the same passphrase.
</info-tooltip>
</template>
</v-radio>
<v-radio v-else :value="PassphraseOption.UseExistingPassphrase">
<template #label>
Use the current passphrase
<info-tooltip>
Create this access with the same passphrase you use for this project.
This allows you to manage existing data you have uploaded with the same passphrase.
</info-tooltip>
</template>
</v-radio>
<v-btn
class="align-self-start"
variant="text"
color="default"
:append-icon="areAdvancedOptionsShown ? '$collapse' : '$expand'"
:disabled="isAdvancedOptionSelected"
@click="areAdvancedOptionsShown = !areAdvancedOptionsShown"
>
Advanced
</v-btn>
<v-expand-transition>
<div v-show="areAdvancedOptionsShown">
<v-radio :value="PassphraseOption.EnterNewPassphrase">
<template #label>
Enter a new passphrase
<info-tooltip>
Create this access with a new encryption passphrase that you can enter on the next step.
The access will not be able to manage any existing data.
</info-tooltip>
</template>
</v-radio>
<v-radio label="" :value="PassphraseOption.GenerateNewPassphrase">
<template #label>
Generate a 12-word passphrase
<info-tooltip>
Create this access with a new encryption passphrase that will be generated for you on the next step.
The access will not be able to manage any existing data.
</info-tooltip>
</template>
</v-radio>
</div>
</v-expand-transition>
</v-radio-group>
</v-col>
<v-expand-transition>
<v-col v-show="areAdvancedOptionsShown" cols="12">
<v-alert type="warning" variant="tonal" rounded="xlg">
Creating a new passphrase for this access will prevent it from accessing any data
that has been uploaded with the current passphrase.
</v-alert>
</v-col>
</v-expand-transition>
</v-row>
</v-form>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import {
VForm,
VRow,
VCol,
VRadioGroup,
VRadio,
VBtn,
VExpandTransition,
VAlert,
} from 'vuetify/components';
import { PassphraseOption } from '@/types/createAccessGrant';
import { CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { RequiredRule } from '@poc/types/common';
import InfoTooltip from '@poc/components/dialogs/createAccessSteps/InfoTooltip.vue';
const emit = defineEmits<{
'selectOption': [option: PassphraseOption];
'passphraseChanged': [passphrase: string];
}>();
const form = ref<VForm | null>(null);
const passphraseOption = ref<PassphraseOption>();
watch(passphraseOption, value => value && emit('selectOption', value));
const bucketsStore = useBucketsStore();
/**
* Indicates whether the user should be prompted to enter the project passphrase.
*/
const isPromptForPassphrase = computed<boolean>(() => bucketsStore.state.promptForPassphrase);
const areAdvancedOptionsShown = ref<boolean>(isPromptForPassphrase.value);
/**
* Indicates whether an option in the Advanced menu has been selected.
*/
const isAdvancedOptionSelected = computed<boolean>(() => {
return passphraseOption.value === PassphraseOption.EnterNewPassphrase
|| passphraseOption.value === PassphraseOption.GenerateNewPassphrase;
});
defineExpose<CreateAccessStepComponent>({
title: 'Access Encryption',
validate: () => {
form.value?.validate();
return !!form.value?.isValid;
},
onExit: () => {
if (passphraseOption.value !== PassphraseOption.UseExistingPassphrase) return;
emit('passphraseChanged', bucketsStore.state.passphrase);
},
});
</script>

View File

@ -0,0 +1,64 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pa-8">
<v-row>
<v-col cols="12">
Copy or save the satellite address and API key as they will only appear once.
</v-col>
<save-buttons :items="saveItems" :access-name="name" file-name-base="CLI-access" />
<v-divider class="my-3" />
<v-col cols="12">
<text-output-area
label="Satellite Address"
:value="satelliteAddress"
:tooltip-disabled="isTooltipDisabled"
show-copy
/>
</v-col>
<v-col cols="12">
<text-output-area
label="API Key"
:value="apiKey"
:tooltip-disabled="isTooltipDisabled"
show-copy
/>
</v-col>
</v-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { VRow, VCol, VDivider } from 'vuetify/components';
import { useConfigStore } from '@/store/modules/configStore';
import { CreateAccessStepComponent, SaveButtonsItem } from '@poc/types/createAccessGrant';
import TextOutputArea from '@poc/components/dialogs/createAccessSteps/TextOutputArea.vue';
import SaveButtons from '@poc/components/dialogs/createAccessSteps/SaveButtons.vue';
const props = defineProps<{
name: string;
apiKey: string;
}>();
const configStore = useConfigStore();
const isTooltipDisabled = ref<boolean>(false);
const satelliteAddress = computed<string>(() => configStore.state.config.satelliteNodeURL);
const saveItems = computed<SaveButtonsItem[]>(() => [
{ name: 'Satellite Address', value: satelliteAddress.value },
{ name: 'API Key', value: props.apiKey },
]);
defineExpose<CreateAccessStepComponent>({
title: 'CLI Access Created',
onEnter: () => isTooltipDisabled.value = false,
onExit: () => isTooltipDisabled.value = true,
});
</script>

View File

@ -0,0 +1,294 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-form ref="form" class="pa-8">
<v-row>
<v-col cols="12">
<v-select
v-model="permissions"
:items="allPermissions"
label="Permissions"
variant="outlined"
color="default"
multiple
chips
closable-chips
hide-details="auto"
:rules="[ RequiredRule ]"
>
<template #prepend-item>
<v-list-item
title="All permissions"
color="primary"
:active="areAllPermsSelected"
@click="toggleSelectedPerms"
>
<template #prepend>
<v-checkbox-btn
v-model="areAllPermsSelected"
:indeterminate="permissions.length != 0 && !areAllPermsSelected"
color="primary"
/>
</template>
</v-list-item>
<v-divider />
</template>
<template #item="{ props: slotProps }">
<v-list-item v-bind="slotProps" color="primary">
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" color="primary" />
</template>
</v-list-item>
</template>
</v-select>
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="buckets"
v-model:search="bucketSearch"
class="choose-permissions-step__buckets-field"
:items="allBucketNames"
label="Buckets"
variant="outlined"
color="default"
no-data-text="No buckets found."
:placeholder="isAllBucketsSelected ? 'All buckets' : undefined"
:persistent-placeholder="isAllBucketsSelected"
multiple
chips
closable-chips
hide-details="auto"
:rules="bucketsRules"
:custom-filter="bucketFilter"
>
<template #prepend-item>
<v-list-item
title="All buckets"
color="primary"
:active="isAllBucketsSelected"
@click="isAllBucketsSelected = !isAllBucketsSelected"
>
<template #prepend>
<v-checkbox-btn v-model="isAllBucketsSelected" color="primary" />
</template>
</v-list-item>
<v-divider />
</template>
<template #item="{ props: slotProps }">
<v-list-item v-bind="slotProps" color="primary">
<template #prepend="{ isSelected }">
<v-checkbox-btn :model-value="isSelected" color="primary" />
</template>
</v-list-item>
</template>
</v-autocomplete>
</v-col>
<v-col cols="12">
<v-select
ref="endDateSelector"
v-model="endDate"
variant="outlined"
color="default"
label="End date"
return-object
hide-details="auto"
:items="endDateItems"
:rules="[ RequiredRule ]"
>
<template #append-inner>
<v-btn
class="choose-permissions-step__date-picker"
icon="$calendar"
variant="text"
color="default"
@mousedown.stop="isDatePicker = true"
/>
</template>
<template #item="{ item, props: itemProps }">
<v-divider v-if="(item.raw as AccessGrantEndDate).date === null" />
<v-list-item v-bind="itemProps" />
</template>
</v-select>
</v-col>
</v-row>
<v-overlay v-model="isDatePicker" class="align-center justify-center">
<v-date-picker
v-model="datePickerModel"
@click:cancel="isDatePicker = false"
@update:model-value="onDatePickerSubmit"
>
<template #header />
</v-date-picker>
</v-overlay>
</v-form>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import {
VForm,
VRow,
VCol,
VBtn,
VDivider,
VSelect,
VAutocomplete,
VListItem,
VCheckboxBtn,
VOverlay,
} from 'vuetify/components';
import { VDatePicker } from 'vuetify/labs/components';
import { Permission } from '@/types/createAccessGrant';
import { AccessGrantEndDate, CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { SHORT_MONTHS_NAMES } from '@/utils/constants/date';
import { ValidationRule, RequiredRule } from '@poc/types/common';
type EndDateListItem = AccessGrantEndDate | { divider: true };
const allPermissions: Permission[] = [
Permission.Read,
Permission.Write,
Permission.List,
Permission.Delete,
];
const endDateItems: EndDateListItem[] = [
{ title: '1 day', date: getNowOffset(1) },
{ title: '1 week', date: getNowOffset(7) },
{ title: '1 month', date: getNowOffset(0, 1) },
{ title: '6 months', date: getNowOffset(0, 6) },
{ title: '1 year', date: getNowOffset(0, 0, 1) },
{ title: 'No end date', date: null },
];
const emit = defineEmits<{
'permissionsChanged': [perms: Permission[]];
'bucketsChanged': [buckets: string[]];
'endDateChanged': [endDate: AccessGrantEndDate];
}>();
const form = ref<VForm | null>(null);
const endDateSelector = ref<VSelect | null>(null);
const permissions = ref<Permission[]>([]);
const buckets = ref<string[]>([]);
const bucketSearch = ref<string>('');
const isAllBucketsSelected = ref<boolean>(false);
const endDate = ref<AccessGrantEndDate | null>(null);
const isDatePicker = ref<boolean>(false);
const datePickerModel = ref<Date[]>([]);
watch(permissions, value => emit('permissionsChanged', value.slice()), { deep: true });
watch(buckets, value => {
emit('bucketsChanged', value.slice());
if (value.length) isAllBucketsSelected.value = false;
}, { deep: true });
watch(endDate, value => value && emit('endDateChanged', value));
watch(isAllBucketsSelected, value => value && (buckets.value = []));
const bucketsRules: ValidationRule<string[]>[] = [ v => (!!v.length || isAllBucketsSelected.value) || 'Required' ];
const bucketsStore = useBucketsStore();
/**
* Indicates whether all permissions have been selected.
*/
const areAllPermsSelected = computed<boolean>(() => permissions.value.length === allPermissions.length);
/**
* Returns all bucket names from the store.
*/
const allBucketNames = computed<string[]>(() => bucketsStore.state.allBucketNames);
/**
* Returns whether the bucket name satisfies the query.
*/
function bucketFilter(bucketName: string, query: string): boolean {
query = query.trim();
if (!query) return true;
let lastIdx = 0;
for (const part of query.split(' ')) {
const idx = bucketName.indexOf(part, lastIdx);
if (idx === -1) return false;
lastIdx = idx + part.length;
}
return true;
}
/**
* Selects or deselects all permissions.
*/
function toggleSelectedPerms(): void {
if (permissions.value.length !== allPermissions.length) {
permissions.value = allPermissions.slice();
return;
}
permissions.value = [];
}
/**
* Returns the current date offset by the specified amount.
*/
function getNowOffset(days = 0, months = 0, years = 0): Date {
const now = new Date();
return new Date(
now.getFullYear() + years,
now.getMonth() + months,
now.getDate() + days,
11, 59, 59,
);
}
/**
* Stores the access grant end date from the date picker.
*/
function onDatePickerSubmit(): void {
if (!datePickerModel.value.length) return;
const date = datePickerModel.value[0];
endDate.value = {
title: `${date.getDate()} ${SHORT_MONTHS_NAMES[date.getMonth()]} ${date.getFullYear()}`,
date: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 11, 59, 59),
};
isDatePicker.value = false;
}
defineExpose<CreateAccessStepComponent>({
title: 'Access Permissions',
validate: () => {
form.value?.validate();
return !!form.value?.isValid;
},
});
</script>
<style scoped lang="scss">
.v-field {
&:not(.v-field--error) .choose-permissions-step__date-picker :deep(.v-icon) {
opacity: var(--v-medium-emphasis-opacity);
}
&.v-field--error .choose-permissions-step__date-picker {
color: rgb(var(--v-theme-error));
}
}
.choose-permissions-step__buckets-field :deep(input::placeholder) {
opacity: 1;
}
</style>

View File

@ -0,0 +1,61 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pa-8">
<v-row>
<v-col cols="12">
Confirm that the access details are correct before creating.
</v-col>
<v-divider class="my-1" />
<v-col class="pa-1" cols="12">
<div
v-for="item in items"
:key="item.title"
class="d-flex justify-space-between ma-2"
>
<p class="flex-shrink-0 mr-4">{{ item.title }}</p>
<p class="text-body-2 text-medium-emphasis text-right">{{ item.value }}</p>
</div>
</v-col>
</v-row>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { VRow, VCol, VDivider } from 'vuetify/components';
import { Permission, AccessType } from '@/types/createAccessGrant';
import { AccessGrantEndDate, CreateAccessStepComponent } from '@poc/types/createAccessGrant';
interface Item {
title: string;
value: string;
}
const props = defineProps<{
name: string;
types: AccessType[];
permissions: Permission[];
buckets: string[];
endDate: AccessGrantEndDate;
}>();
/**
* Returns the data used to generate the info rows.
*/
const items = computed<Item[]>(() => {
return [
{ title: 'Name', value: props.name },
{ title: 'Type', value: props.types.join(', ') },
{ title: 'Permissions', value: props.permissions.join(', ') },
{ title: 'Buckets', value: props.buckets.join(', ') || 'All buckets' },
{ title: 'End Date', value: props.endDate.title },
];
});
defineExpose<CreateAccessStepComponent>({
title: 'Confirm Details',
});
</script>

View File

@ -0,0 +1,137 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-form ref="form" class="pa-8">
<v-row>
<v-col cols="12">
<v-text-field
v-model="name"
label="Access Name"
placeholder="Enter name for this access"
variant="outlined"
color="default"
autofocus
hide-details="auto"
:rules="nameRules"
/>
</v-col>
<v-col cols="12">
<h4 class="mb-2">Type</h4>
<v-input v-model="types" :rules="[ RequiredRule ]" hide-details="auto">
<div>
<v-checkbox
v-for="accessType in typeOrder"
:key="accessType"
v-model="typeInfos[accessType].model.value"
color="primary"
density="compact"
:hide-details="true"
>
<template #label>
<span class="ml-2">{{ typeInfos[accessType].name }}</span>
<info-tooltip>
{{ typeInfos[accessType].description }}
<a class="text-surface" :href="ACCESS_TYPE_LINKS[accessType]" target="_blank">
Learn more
</a>
</info-tooltip>
</template>
</v-checkbox>
</div>
</v-input>
</v-col>
</v-row>
</v-form>
</template>
<script setup lang="ts">
import { computed, ref, watch, WritableComputedRef } from 'vue';
import { VForm, VRow, VCol, VTextField, VCheckbox, VInput } from 'vuetify/components';
import { AccessType } from '@/types/createAccessGrant';
import { ACCESS_TYPE_LINKS, CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { RequiredRule, ValidationRule } from '@poc/types/common';
import InfoTooltip from '@poc/components/dialogs/createAccessSteps/InfoTooltip.vue';
class AccessTypeInfo {
public model: WritableComputedRef<boolean>;
constructor(
public accessType: AccessType,
public name: string,
public description: string,
public exclusive: boolean = false,
) {
this.model = computed<boolean>({
get: () => types.value.includes(accessType),
set: (checked: boolean) => {
if (!checked) {
types.value = types.value.filter(iterType => iterType !== accessType);
return;
}
if (typeInfos[this.accessType].exclusive) {
types.value = [this.accessType];
return;
}
types.value = [...types.value.filter(iter => !typeInfos[iter].exclusive), accessType];
},
});
}
}
const typeInfos: Record<AccessType, AccessTypeInfo> = {
[AccessType.AccessGrant]: new AccessTypeInfo(
AccessType.AccessGrant,
'Access Grant',
'Keys to upload, delete, and view your data.',
),
[AccessType.S3]: new AccessTypeInfo(
AccessType.S3,
'S3 Credentials',
'Generates access key, secret key, and endpoint to use in your S3 supported application.',
),
[AccessType.APIKey]: new AccessTypeInfo(
AccessType.APIKey,
'CLI Access',
'Create an access grant to run in the command line.',
true,
),
};
const typeOrder: AccessType[] = [
AccessType.AccessGrant,
AccessType.S3,
AccessType.APIKey,
];
const form = ref<VForm | null>(null);
const name = ref<string>('');
const types = ref<AccessType[]>([]);
watch(name, value => emit('nameChanged', value));
watch(types, value => emit('typesChanged', value.slice()), { deep: true });
const emit = defineEmits<{
'nameChanged': [name: string];
'typesChanged': [types: AccessType[]];
}>();
const agStore = useAccessGrantsStore();
const nameRules: ValidationRule<string>[] = [
RequiredRule,
v => !agStore.state.allAGNames.includes(v) || 'This name is already in use',
];
defineExpose<CreateAccessStepComponent>({
title: 'Create New Access',
validate: () => {
form.value?.validate();
return !!form.value?.isValid;
},
});
</script>

View File

@ -0,0 +1,43 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-form ref="form" class="pa-8">
<v-row>
<v-col cols="12">
By generating S3 credentials, you are opting in to
<a class="link" href="https://docs.storj.io/dcs/concepts/encryption-key/design-decision-server-side-encryption/">
server-side encryption.
</a>
</v-col>
<v-col cols="12">
<v-checkbox
density="compact"
label="I understand, don't show this again."
hide-details="auto"
:rules="[ RequiredRule ]"
@update:model-value="value => LocalData.setServerSideEncryptionModalHidden(value)"
/>
</v-col>
</v-row>
</v-form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { VForm, VRow, VCol, VCheckbox } from 'vuetify/components';
import { LocalData } from '@/utils/localData';
import { RequiredRule } from '@poc/types/common';
import { CreateAccessStepComponent } from '@poc/types/createAccessGrant';
const form = ref<VForm | null>(null);
defineExpose<CreateAccessStepComponent>({
title: 'Encryption Information',
validate: () => {
form.value?.validate();
return !!form.value?.isValid;
},
});
</script>

View File

@ -0,0 +1,73 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-form ref="form" class="pa-8">
<v-row>
<v-col cols="12">
<p v-if="passphraseType === CreateAccessStep.EnterMyPassphrase">
Enter the encryption passphrase used for this project to create this access grant.
</p>
<p v-else>
This passphrase will be used to encrypt all the files you upload using this access grant.
You will need it to access these files in the future.
</p>
</v-col>
<v-col cols="12">
<v-text-field
v-model="passphrase"
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-col v-if="passphraseType !== CreateAccessStep.EnterMyPassphrase" cols="12">
<v-checkbox
density="compact"
color="primary"
label="Yes, I saved my encryption passphrase."
hide-details="auto"
:rules="[ RequiredRule ]"
/>
</v-col>
</v-row>
</v-form>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { VForm, VRow, VCol, VTextField, VCheckbox } from 'vuetify/components';
import { CreateAccessStep } from '@/types/createAccessGrant';
import { CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import { RequiredRule } from '@poc/types/common';
const props = defineProps<{
passphraseType: CreateAccessStep.EnterMyPassphrase | CreateAccessStep.EnterNewPassphrase,
}>();
const form = ref<VForm | null>(null);
const passphrase = ref<string>('');
const isPassphraseVisible = ref<boolean>(false);
const emit = defineEmits<{
'passphraseChanged': [passphrase: string];
}>();
watch(passphrase, value => emit('passphraseChanged', value));
defineExpose<CreateAccessStepComponent>({
title: props.passphraseType === CreateAccessStep.EnterMyPassphrase ? 'Enter Passphrase' : 'Enter New Passphrase',
validate: () => {
form.value?.validate();
return !!form.value?.isValid;
},
});
</script>

View File

@ -0,0 +1,28 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<span class="d-inline-flex ml-2" @click.prevent="isOpen = true">
<icon-info />
<v-tooltip
v-model="isOpen"
class="text-center"
activator="parent"
location="top"
max-width="300px"
open-delay="150"
close-delay="150"
>
<slot />
</v-tooltip>
</span>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { VTooltip } from 'vuetify/components';
import IconInfo from '@poc/components/icons/IconInfo.vue';
const isOpen = ref<boolean>(false);
</script>

View File

@ -0,0 +1,72 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-form ref="form" class="pa-8">
<v-row>
<v-col cols="12">
This passphrase will be used to encrypt all the files you upload using this access grant.
You will need it to access these files in the future.
</v-col>
<save-buttons :access-name="name" :items="[ passphrase ]" file-name-base="passphrase" />
<v-divider class="my-3" />
<v-col cols="12">
<text-output-area
label="Encryption Passphrase"
:value="passphrase"
center-text
:tooltip-disabled="isTooltipDisabled"
show-copy
/>
</v-col>
<v-col cols="12">
<v-checkbox
density="compact"
color="primary"
label="Yes, I saved my encryption passphrase."
hide-details="auto"
:rules="[ RequiredRule ]"
/>
</v-col>
</v-row>
</v-form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { generateMnemonic } from 'bip39-english';
import { VForm, VRow, VCol, VCheckbox, VDivider } from 'vuetify/components';
import { RequiredRule } from '@poc/types/common';
import { CreateAccessStepComponent } from '@poc/types/createAccessGrant';
import TextOutputArea from '@poc/components/dialogs/createAccessSteps/TextOutputArea.vue';
import SaveButtons from '@poc/components/dialogs/createAccessSteps/SaveButtons.vue';
const props = defineProps<{
name: string;
}>();
const emit = defineEmits<{
'passphraseChanged': [passphrase: string];
}>();
const form = ref<VForm | null>(null);
const isTooltipDisabled = ref<boolean>(false);
const passphrase: string = generateMnemonic();
defineExpose<CreateAccessStepComponent>({
title: 'Passphrase Generated',
onEnter: () => {
emit('passphraseChanged', passphrase);
isTooltipDisabled.value = false;
},
onExit: () => isTooltipDisabled.value = true,
validate: () => {
form.value?.validate();
return !!form.value?.isValid;
},
});
</script>

View File

@ -0,0 +1,55 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="pa-8">
<v-row>
<v-col cols="12">
Copy or save the S3 credentials as they will only appear once.
</v-col>
<save-buttons :items="saveItems" :access-name="name" file-name-base="S3-credentials" />
<v-divider class="my-3" />
<v-col cols="12">
<text-output-area label="Access Key" :value="accessKey" :tooltip-disabled="isTooltipDisabled" show-copy />
</v-col>
<v-col cols="12">
<text-output-area label="Secret Key" :value="secretKey" :tooltip-disabled="isTooltipDisabled" show-copy />
</v-col>
<v-col cols="12">
<text-output-area label="Endpoint" :value="endpoint" :tooltip-disabled="isTooltipDisabled" show-copy />
</v-col>
</v-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { VRow, VCol, VDivider } from 'vuetify/components';
import { SaveButtonsItem } from '@poc/types/createAccessGrant';
import TextOutputArea from '@poc/components/dialogs/createAccessSteps/TextOutputArea.vue';
import SaveButtons from '@poc/components/dialogs/createAccessSteps/SaveButtons.vue';
const props = defineProps<{
name: string;
accessKey: string;
secretKey: string;
endpoint: string;
}>();
const isTooltipDisabled = ref<boolean>(false);
const saveItems = computed<SaveButtonsItem[]>(() => [
{ name: 'Access Key', value: props.accessKey },
{ name: 'Secret Key', value: props.secretKey },
{ name: 'Endpoint', value: props.endpoint },
]);
defineExpose({
title: 'Credentials Generated',
onEnter: () => isTooltipDisabled.value = false,
onExit: () => isTooltipDisabled.value = true,
});
</script>

View File

@ -0,0 +1,83 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-col cols="6">
<v-btn
variant="outlined"
size="small"
block
:color="justCopied ? 'success' : 'default'"
:prepend-icon="justCopied ? 'mdi-check' : 'mdi-content-copy'"
@click="onCopy"
>
{{ justCopied ? 'Copied' : (items.length > 1 ? 'Copy All' : 'Copy') }}
</v-btn>
</v-col>
<v-col cols="6">
<v-btn
variant="outlined"
size="small"
block
:color="justDownloaded ? 'success' : 'default'"
:prepend-icon="justDownloaded ? 'mdi-check' : 'mdi-tray-arrow-down'"
@click="onDownload"
>
{{ justDownloaded ? 'Downloaded' : (items.length > 1 ? 'Download All' : 'Download') }}
</v-btn>
</v-col>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { VCol, VBtn } from 'vuetify/components';
import { SaveButtonsItem } from '@poc/types/createAccessGrant';
import { Download } from '@/utils/download';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
const props = defineProps<{
items: SaveButtonsItem[];
accessName: string;
fileNameBase: string;
}>();
const successDuration = 2000;
const copiedTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
const downloadedTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
const justCopied = computed<boolean>(() => copiedTimeout.value !== null);
const justDownloaded = computed<boolean>(() => downloadedTimeout.value !== null);
const analyticsStore = useAnalyticsStore();
/**
* Saves items to clipboard.
*/
function onCopy(): void {
navigator.clipboard.writeText(props.items.map(item => typeof item === 'string' ? item : item.value).join(' '));
analyticsStore.eventTriggered(AnalyticsEvent.COPY_TO_CLIPBOARD_CLICKED);
if (copiedTimeout.value) clearTimeout(copiedTimeout.value);
copiedTimeout.value = setTimeout(() => {
copiedTimeout.value = null;
}, successDuration);
}
/**
* Downloads items into .txt file.
*/
function onDownload(): void {
Download.file(
props.items.map(item => typeof item === 'string' ? item : `${item.name}:\n${item.value}`).join('\n\n'),
`Storj-${props.fileNameBase}-${props.accessName}-${new Date().toISOString()}.txt`,
);
analyticsStore.eventTriggered(AnalyticsEvent.DOWNLOAD_TXT_CLICKED);
if (downloadedTimeout.value) clearTimeout(downloadedTimeout.value);
downloadedTimeout.value = setTimeout(() => {
downloadedTimeout.value = null;
}, successDuration);
}
</script>

View File

@ -0,0 +1,133 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-textarea
class="text-output-area"
:class="{
'text-output-area--center-text': centerText,
'text-output-area--unblur': !isBlurred,
}"
variant="solo-filled"
:label="label"
:model-value="value"
rows="1"
auto-grow
no-resize
readonly
hide-details
flat
>
<template #prepend-inner>
<v-fade-transition>
<div v-show="isBlurred" class="text-output-area__show">
<v-btn
class="bg-background"
variant="outlined"
color="default"
size="small"
prepend-icon="mdi-lock-outline"
@click="isBlurred = false"
>
Show {{ label }}
</v-btn>
</div>
</v-fade-transition>
</template>
<template v-if="showCopy" #append-inner>
<v-tooltip v-model="isTooltip" location="start">
<template #activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
:icon="justCopied ? 'mdi-check' : 'mdi-content-copy'"
variant="text"
density="compact"
:color="justCopied ? 'success' : 'default'"
@click="onCopy"
/>
</template>
{{ justCopied ? 'Copied!' : 'Copy' }}
</v-tooltip>
</template>
</v-textarea>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { VTextarea, VFadeTransition, VBtn, VTooltip } from 'vuetify/components';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
const props = defineProps<{
label: string;
value: string;
centerText?: boolean;
tooltipDisabled?: boolean;
showCopy?: boolean;
}>();
const isBlurred = ref<boolean>(true);
const copiedTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
const justCopied = computed<boolean>(() => copiedTimeout.value !== null);
const isTooltip = (() => {
const internal = ref<boolean>(false);
return computed<boolean>({
get: () => (internal.value || justCopied.value) && !props.tooltipDisabled,
set: v => internal.value = v,
});
})();
const analyticsStore = useAnalyticsStore();
/**
* Saves value to clipboard.
*/
function onCopy(): void {
navigator.clipboard.writeText(props.value);
analyticsStore.eventTriggered(AnalyticsEvent.COPY_TO_CLIPBOARD_CLICKED);
if (copiedTimeout.value) clearTimeout(copiedTimeout.value);
copiedTimeout.value = setTimeout(() => {
copiedTimeout.value = null;
}, 750);
}
</script>
<style scoped lang="scss">
.text-output-area {
&__show {
position: absolute;
z-index: 1;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: default;
}
:deep(textarea) {
font-family: monospace;
}
:deep(.v-field__field), :deep(.v-field__append-inner) {
filter: blur(10px);
}
&--unblur {
:deep(.v-field__field), :deep(.v-field__append-inner) {
filter: none;
transition: filter 0.25s ease;
}
}
&--center-text :deep(textarea) {
text-align: center;
}
}
</style>

View File

@ -0,0 +1,18 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg :width="size" :height="size" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path :fill="color" d="M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35ZM9.25 13.0745V10.429C9.25 9.97337 9.61937 9.604 10.075 9.604C10.5195 9.604 10.8819 9.95557 10.8993 10.3958L10.9 10.429V13.0196C10.9 13.4799 10.535 13.8572 10.075 13.8726C9.63427 13.8872 9.26511 13.5418 9.25044 13.1011C9.25015 13.0922 9.25 13.0834 9.25 13.0745ZM9.25 6.92454V6.829C9.25 6.37337 9.61937 6.004 10.075 6.004C10.5195 6.004 10.8819 6.35557 10.8993 6.79582L10.9 6.829V6.86964C10.9 7.32991 10.535 7.70724 10.075 7.72255C9.63427 7.73721 9.26511 7.39182 9.25044 6.9511C9.25015 6.94225 9.25 6.9334 9.25 6.92454Z" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
color: string;
size: number;
}>(), {
color: 'currentColor',
size: 16,
});
</script>

View File

@ -31,6 +31,7 @@ import { useABTestingStore } from '@/store/modules/abTestingStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAppStore } from '@poc/store/appStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
@ -44,6 +45,7 @@ const usersStore = useUsersStore();
const abTestingStore = useABTestingStore();
const projectsStore = useProjectsStore();
const appStore = useAppStore();
const agStore = useAccessGrantsStore();
const isLoading = ref<boolean>(true);
@ -109,5 +111,7 @@ onBeforeMount(async () => {
}
selectProject(route.params.projectId as string);
if (!agStore.state.accessGrantsWebWorker) await agStore.startWorker();
});
</script>

View File

@ -12,6 +12,10 @@
@use './settings';
@import 'static/styles/variables';
html {
overflow-y: auto !important;
}
// Light Theme
.v-theme--light {
--v-border-color: 0, 0, 0;
@ -200,6 +204,10 @@
border-radius: 8px;
}
// Ensure that the compact checkbox isn't too close to its label
.v-selection-control--density-compact .v-label {
margin-left: 8px;
}
// Sorting icon
.mdi-arrow-up::before, .mdi-arrow-down::before {
@ -278,9 +286,23 @@ table {
border-radius: 20px !important;
padding: 6px 14px;
backdrop-filter: blur(12px);
pointer-events: all !important;
}
// Upload Snackbar
.upload-snackbar .v-snackbar__content {
padding: 2px;
}
// Scrollbar
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background-color: rgb(var(--v-theme-background));
}
::-webkit-scrollbar-thumb {
background-color: rgba(var(--v-theme-on-background), 0.33);
border-radius: 2px;
min-height: 5px;
}

View File

@ -0,0 +1,7 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
export type ValidationRule<T> = string | boolean | ((value: T) => string | boolean);
export function RequiredRule(value: unknown): string | boolean {
return (Array.isArray(value) ? !!value.length : !!value) || 'Required';
}

View File

@ -0,0 +1,27 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { AccessType } from '@/types/createAccessGrant';
export const ACCESS_TYPE_LINKS: Record<AccessType, string> = {
[AccessType.AccessGrant]: 'https://docs.storj.io/dcs/concepts/access/access-grants',
[AccessType.S3]: 'https://docs.storj.io/dcs/api-reference/s3-compatible-gateway',
[AccessType.APIKey]: 'https://docs.storj.io/dcs/getting-started/quickstart-uplink-cli/generate-access-grants-and-tokens/generate-a-token',
};
export interface AccessGrantEndDate {
title: string;
date: Date | null;
}
export interface CreateAccessStepComponent {
title: string;
onEnter?: () => void;
onExit?: () => void;
validate?: () => boolean;
}
export type SaveButtonsItem = string | {
name: string;
value: string;
};

View File

@ -8,181 +8,18 @@
<v-col>
<v-row class="mt-2 mb-4">
<v-btn>
<!-- <svg width="16" height="16" viewBox="0 0 18 18" fill="none" class="mr-2" xmlns="http://www.w3.org/2000/svg">
<path d="M4.83987 16.8886L1.47448 17.099C1.17636 17.1176 0.919588 16.891 0.900956 16.5929C0.899551 16.5704 0.899551 16.5479 0.900956 16.5254L1.11129 13.16C1.11951 13.0285 1.17546 12.9045 1.26864 12.8114L5.58927 8.49062L5.57296 8.43619C4.98999 6.44548 5.49345 4.26201 6.96116 2.72323L7.00936 2.67328L7.05933 2.62271C9.35625 0.325796 13.0803 0.325796 15.3772 2.62271C17.6741 4.91963 17.6741 8.64366 15.3772 10.9406C13.8503 12.4674 11.6456 13.0112 9.62856 12.4455L9.56357 12.4269L9.50918 12.4107L5.18856 16.7313C5.09538 16.8244 4.97139 16.8804 4.83987 16.8886ZM2.45229 15.5477L4.38997 15.4266L9.13372 10.6827L9.58862 10.864C11.2073 11.5091 13.072 11.1424 14.3255 9.88889C16.0416 8.17281 16.0416 5.39048 14.3255 3.6744C12.6094 1.95831 9.8271 1.95831 8.11101 3.6744C6.87177 4.91364 6.49924 6.7502 7.11424 8.3559L7.13584 8.41118L7.31711 8.86605L2.57342 13.61L2.45229 15.5477ZM10.7858 7.21411C11.3666 7.79494 12.3083 7.79494 12.8892 7.21411C13.47 6.63328 13.47 5.69157 12.8892 5.11074C12.3083 4.52991 11.3666 4.52991 10.7858 5.11074C10.205 5.69157 10.205 6.63328 10.7858 7.21411Z" fill="currentColor"/>
</svg> -->
<v-btn @click="dialog = true">
<svg width="16" height="16" class="mr-2" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1ZM10 2.65C5.94071 2.65 2.65 5.94071 2.65 10C2.65 14.0593 5.94071 17.35 10 17.35C14.0593 17.35 17.35 14.0593 17.35 10C17.35 5.94071 14.0593 2.65 10 2.65ZM10.7496 6.8989L10.7499 6.91218L10.7499 9.223H12.9926C13.4529 9.223 13.8302 9.58799 13.8456 10.048C13.8602 10.4887 13.5148 10.8579 13.0741 10.8726L13.0608 10.8729L10.7499 10.873L10.75 13.171C10.75 13.6266 10.3806 13.996 9.925 13.996C9.48048 13.996 9.11807 13.6444 9.10066 13.2042L9.1 13.171L9.09985 10.873H6.802C6.34637 10.873 5.977 10.5036 5.977 10.048C5.977 9.60348 6.32857 9.24107 6.76882 9.22366L6.802 9.223H9.09985L9.1 6.98036C9.1 6.5201 9.46499 6.14276 9.925 6.12745C10.3657 6.11279 10.7349 6.45818 10.7496 6.8989Z" fill="currentColor" />
</svg>
New Access
<v-dialog
v-model="dialog"
activator="parent"
min-width="400px"
width="auto"
transition="fade-transition"
scrollable
>
<v-card rounded="xlg">
<v-sheet>
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-card-title class="font-weight-bold">
<!-- <v-icon>
<img src="../assets/icon-team.svg" alt="Team">
</v-icon> -->
{{ stepTitles[step] }}
</v-card-title>
</template>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
@click="dialog = false"
/>
</template>
</v-card-item>
</v-sheet>
<v-divider />
<v-window v-model="step">
<v-window-item :value="0">
<v-form class="pa-8 pb-3">
<v-row>
<v-col cols="12">
<!-- <h4 class="mb-2">Name</h4> -->
<v-text-field
label="Access Name"
placeholder="Enter name for this access"
variant="outlined"
color="default"
autofocus
/>
</v-col>
<v-col>
<h4 class="mb-2">Type</h4>
<v-checkbox color="primary" density="compact">
<template #label>
<span class="mx-2">Access Grant</span>
<span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" tabindex="0"><path d="M8 15.2A7.2 7.2 0 118 .8a7.2 7.2 0 010 14.4zm0-1.32A5.88 5.88 0 108 2.12a5.88 5.88 0 000 11.76zm-.6-3.42V8.343a.66.66 0 011.32-.026V10.416c0 .368-.292.67-.66.682a.639.639 0 01-.66-.638zm0-4.92v-.077a.66.66 0 011.32-.026v.059c0 .368-.292.67-.66.682a.639.639 0 01-.66-.638z" fill="currentColor" /></svg>
<v-tooltip activator="parent" location="top">
<span>Keys to upload, delete, and view your data. Learn more</span>
</v-tooltip>
</span>
</template>
</v-checkbox>
<v-checkbox color="primary" density="compact">
<template #label>
<span class="mx-2">S3 Credentials</span>
<span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" tabindex="0"><path d="M8 15.2A7.2 7.2 0 118 .8a7.2 7.2 0 010 14.4zm0-1.32A5.88 5.88 0 108 2.12a5.88 5.88 0 000 11.76zm-.6-3.42V8.343a.66.66 0 011.32-.026V10.416c0 .368-.292.67-.66.682a.639.639 0 01-.66-.638zm0-4.92v-.077a.66.66 0 011.32-.026v.059c0 .368-.292.67-.66.682a.639.639 0 01-.66-.638z" fill="currentColor" /></svg>
<v-tooltip activator="parent" location="top">
<span>Generates access key, secret key, and endpoint to use in your S3 supported application. Learn More</span>
</v-tooltip>
</span>
</template>
</v-checkbox>
<v-checkbox color="primary" density="compact">
<template #label>
<span class="mx-2">CLI Access</span>
<span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" tabindex="0"><path d="M8 15.2A7.2 7.2 0 118 .8a7.2 7.2 0 010 14.4zm0-1.32A5.88 5.88 0 108 2.12a5.88 5.88 0 000 11.76zm-.6-3.42V8.343a.66.66 0 011.32-.026V10.416c0 .368-.292.67-.66.682a.639.639 0 01-.66-.638zm0-4.92v-.077a.66.66 0 011.32-.026v.059c0 .368-.292.67-.66.682a.639.639 0 01-.66-.638z" fill="currentColor" /></svg>
<v-tooltip activator="parent" location="top">
<span>Create an access grant to run in the command line. Learn more</span>
</v-tooltip>
</span>
</template>
</v-checkbox>
</v-col>
</v-row>
</v-form>
</v-window-item>
<v-window-item :value="1">
<v-form class="pa-8 pb-3">
<v-row>
<v-col cols="12">
<p>Permissions</p>
<p>Buckets</p>
<p>End date</p>
</v-col>
</v-row>
</v-form>
</v-window-item>
<v-window-item :value="2">
<v-form class="pa-8 pb-3">
<v-row>
<v-col cols="12">
<v-text-field
label="Password"
type="password"
variant="outlined"
/>
<v-text-field
label="Confirm Password"
type="password"
variant="outlined"
/>
</v-col>
</v-row>
</v-form>
</v-window-item>
</v-window>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn
v-if="!step"
variant="outlined"
color="default"
block
>
Learn More
</v-btn>
<v-btn
v-else
variant="outlined"
color="default"
block
@click="step--"
>
Back
</v-btn>
</v-col>
<v-col>
<v-btn
v-if="step < 2"
color="primary"
variant="flat"
block
@click="step++"
>
Next
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</v-btn>
</v-row>
</v-col>
<AccessTableComponent />
</v-container>
<NewAccessDialog v-model="dialog" />
</template>
<script setup lang="ts">
@ -192,32 +29,12 @@ import {
VCol,
VRow,
VBtn,
VDialog,
VCard,
VSheet,
VCardItem,
VCardTitle,
VDivider,
VWindow,
VWindowItem,
VForm,
VTextField,
VCheckbox,
VTooltip,
VCardActions,
} from 'vuetify/components';
import NewAccessDialog from '@poc/components/dialogs/CreateAccessDialog.vue';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import PageSubtitleComponent from '@poc/components/PageSubtitleComponent.vue';
import AccessTableComponent from '@poc/components/AccessTableComponent.vue';
const dialog = ref<boolean>(false);
const step = ref<number>(0);
const stepTitles = [
'Create New Access',
'Permissions',
'Passphrase',
'Access Created',
];
</script>