diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/CreateAccessGrantFlow.vue b/web/satellite/src/components/accessGrants/newCreateFlow/CreateAccessGrantFlow.vue index f57402d46..c7397acc2 100644 --- a/web/satellite/src/components/accessGrants/newCreateFlow/CreateAccessGrantFlow.vue +++ b/web/satellite/src/components/accessGrants/newCreateFlow/CreateAccessGrantFlow.vue @@ -27,7 +27,9 @@ :on-select-permission="selectPermissions" :selected-permissions="selectedPermissions" :on-back="setFirstStepBasedOnAccessType" - :on-continue="() => setStep(CreateAccessStep.AccessEncryption)" + :on-continue="() => setStep( + selectedAccessTypes.includes(AccessType.APIKey) ? setLastStep() : CreateAccessStep.AccessEncryption + )" :selected-buckets="selectedBuckets" :on-select-bucket="selectBucket" :on-select-all-buckets="selectAllBuckets" @@ -36,6 +38,7 @@ :on-set-not-after="setNotAfter" :not-after-label="notAfterLabel" :on-set-not-after-label="setNotAfterLabel" + :is-loading="isLoading" /> + + @@ -89,8 +104,14 @@ import { STEP_ICON_AND_TITLE, } from '@/types/createAccessGrant'; import { BUCKET_ACTIONS } from '@/store/modules/buckets'; -import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames'; +import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames'; import { LocalData } from '@/utils/localData'; +import { PROJECTS_ACTIONS } from '@/store/modules/projects'; +import { AccessGrant, EdgeCredentials } from '@/types/accessGrants'; +import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants'; +import { MetaUtils } from '@/utils/meta'; +import { AnalyticsHttpApi } from '@/api/analytics'; +import { OBJECTS_MUTATIONS } from '@/store/modules/objects'; import VModal from '@/components/common/VModal.vue'; import CreateNewAccessStep from '@/components/accessGrants/newCreateFlow/steps/CreateNewAccessStep.vue'; @@ -99,6 +120,7 @@ import AccessEncryptionStep from '@/components/accessGrants/newCreateFlow/steps/ import EnterPassphraseStep from '@/components/accessGrants/newCreateFlow/steps/EnterPassphraseStep.vue'; import PassphraseGeneratedStep from '@/components/accessGrants/newCreateFlow/steps/PassphraseGeneratedStep.vue'; import EncryptionInfoStep from '@/components/accessGrants/newCreateFlow/steps/EncryptionInfoStep.vue'; +import AccessCreatedStep from '@/components/accessGrants/newCreateFlow/steps/AccessCreatedStep.vue'; const router = useRouter(); const route = useRoute(); @@ -126,6 +148,8 @@ const storedPassphrase = computed((): string => { return store.state.objectsModule.passphrase; }); +const worker = ref(); +const isLoading = ref(false); const step = ref(CreateAccessStep.CreateNewAccess); const selectedAccessTypes = ref([]); const selectedPermissions = ref(initPermissions); @@ -139,6 +163,14 @@ const accessName = ref(''); const notAfter = ref(undefined); const notAfterLabel = ref('No end date'); +// Generated values. +const cliAccess = ref(''); +const accessGrant = ref(''); +const edgeCredentials = ref(new EdgeCredentials()); + +const FIRST_PAGE = 1; +const analytics: AnalyticsHttpApi = new AnalyticsHttpApi(); + /** * Selects access type. */ @@ -338,7 +370,7 @@ function setFirstStepBasedOnAccessType(): void { /** * Sets next step depending on selected passphrase option. */ -function setStepBasedOnPassphraseOption(): void { +async function setStepBasedOnPassphraseOption(): Promise { switch (passphraseOption.value) { case PassphraseOption.SetMyProjectPassphrase: step.value = CreateAccessStep.EnterMyPassphrase; @@ -349,9 +381,8 @@ function setStepBasedOnPassphraseOption(): void { case PassphraseOption.GenerateNewPassphrase: step.value = CreateAccessStep.PassphraseGenerated; break; - default: - // TODO: generate access and redirect to access created. - step.value = CreateAccessStep.AccessCreated; + case PassphraseOption.UseExistingPassphrase: + await setLastStep(); } } @@ -362,11 +393,190 @@ function closeModal(): void { router.push(RouteConfig.AccessGrants.path); } +/** + * Sets local worker with worker instantiated in store. + * Also sets worker's onmessage and onerror logic. + */ +function setWorker(): void { + worker.value = store.state.accessGrantsModule.accessGrantsWebWorker; + if (worker.value) { + worker.value.onerror = (error: ErrorEvent) => { + notify.error(error.message, AnalyticsErrorEventSource.CREATE_AG_MODAL); + }; + } +} + +/** + * Generates CLI access. + */ +async function createCLIAccess(): Promise { + if (!worker.value) { + throw new Error('Web worker is not initialized.'); + } + + // creates fresh new API key. + const cleanAPIKey: AccessGrant = await store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, accessName.value); + + try { + await store.dispatch(ACCESS_GRANTS_ACTIONS.FETCH, FIRST_PAGE); + } catch (error) { + await notify.error(`Unable to fetch Access Grants. ${error.message}`, AnalyticsErrorEventSource.CREATE_AG_MODAL); + } + + let permissionsMsg = { + 'type': 'SetPermission', + 'buckets': selectedBuckets.value, + 'apiKey': cleanAPIKey.secret, + 'isDownload': selectedPermissions.value.includes(Permission.Read), + 'isUpload': selectedPermissions.value.includes(Permission.Write), + 'isList': selectedPermissions.value.includes(Permission.List), + 'isDelete': selectedPermissions.value.includes(Permission.Delete), + 'notBefore': new Date().toISOString(), + }; + + if (notAfter.value) permissionsMsg = Object.assign(permissionsMsg, { 'notAfter': notAfter.value.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 (selectedAccessTypes.value.includes(AccessType.APIKey)) { + analytics.eventTriggered(AnalyticsEvent.API_ACCESS_CREATED); + } +} + +/** + * Generates access grant. + */ +async function createAccessGrant(): Promise { + if (!worker.value) { + throw new Error('Web worker is not initialized.'); + } + + // creates access credentials. + const satelliteNodeURL = MetaUtils.getMetaContent('satellite-nodeurl'); + + const salt = await store.dispatch(PROJECTS_ACTIONS.GET_SALT, store.getters.selectedProject.id); + + let usedPassphrase = ''; + switch (passphraseOption.value) { + case PassphraseOption.UseExistingPassphrase: + usedPassphrase = storedPassphrase.value; + break; + case PassphraseOption.EnterNewPassphrase: + case PassphraseOption.SetMyProjectPassphrase: + usedPassphrase = enteredPassphrase.value; + break; + case PassphraseOption.GenerateNewPassphrase: + usedPassphrase = generatedPassphrase.value; + } + + if (!usedPassphrase) { + throw new Error('Passphrase can\'t be empty'); + } + + worker.value.postMessage({ + 'type': 'GenerateAccess', + 'apiKey': cliAccess.value, + 'passphrase': usedPassphrase, + '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 (selectedAccessTypes.value.includes(AccessType.AccessGrant)) { + analytics.eventTriggered(AnalyticsEvent.ACCESS_GRANT_CREATED); + } +} + +/** + * Generates edge credentials. + */ +async function createEdgeCredentials(): Promise { + edgeCredentials.value = await store.dispatch( + ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, { accessGrant: accessGrant.value }, + ); + analytics.eventTriggered(AnalyticsEvent.GATEWAY_CREDENTIALS_CREATED); +} + +/** + * Generates access and sets the last step depending on selected access type. + */ +async function setLastStep(): Promise { + if (isLoading.value) { + return; + } + + isLoading.value = true; + + try { + switch (true) { + case selectedAccessTypes.value.includes(AccessType.APIKey): + await createCLIAccess(); + + step.value = CreateAccessStep.CLIAccessCreated; + break; + case selectedAccessTypes.value.includes(AccessType.AccessGrant) && selectedAccessTypes.value.includes(AccessType.S3): + await createCLIAccess(); + await createAccessGrant(); + await createEdgeCredentials(); + + step.value = CreateAccessStep.AccessCreated; + break; + case selectedAccessTypes.value.includes(AccessType.S3): + await createCLIAccess(); + await createAccessGrant(); + await createEdgeCredentials(); + + step.value = CreateAccessStep.CredentialsCreated; + break; + case selectedAccessTypes.value.includes(AccessType.AccessGrant): + await createCLIAccess(); + await createAccessGrant(); + + step.value = CreateAccessStep.AccessCreated; + } + + // This is an action to handle case if user sets project level passphrase. + if ( + passphraseOption.value === PassphraseOption.SetMyProjectPassphrase && + !selectedAccessTypes.value.includes(AccessType.APIKey) + ) { + store.commit(OBJECTS_MUTATIONS.SET_PASSPHRASE, enteredPassphrase.value); + store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); + } + } catch (error) { + await notify.error(error.message, AnalyticsErrorEventSource.CREATE_AG_MODAL); + } + + isLoading.value = false; +} + onMounted(async () => { if (route.params?.accessType) { selectedAccessTypes.value.push(route.params?.accessType as AccessType); } + setWorker(); generatedPassphrase.value = generateMnemonic(); try { @@ -383,6 +593,7 @@ onMounted(async () => { padding: 32px; display: flex; flex-direction: column; + position: relative; &__header { display: flex; @@ -399,5 +610,15 @@ onMounted(async () => { color: var(--c-black); } } + + &__blur { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + background-color: rgb(0 0 0 / 10%); + border-radius: 10px; + } } diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/components/ValueWithBlur.vue b/web/satellite/src/components/accessGrants/newCreateFlow/components/ValueWithBlur.vue index 7da996eee..03a85ca40 100644 --- a/web/satellite/src/components/accessGrants/newCreateFlow/components/ValueWithBlur.vue +++ b/web/satellite/src/components/accessGrants/newCreateFlow/components/ValueWithBlur.vue @@ -3,19 +3,34 @@ @@ -23,70 +38,146 @@ diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessCreatedStep.vue b/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessCreatedStep.vue new file mode 100644 index 000000000..4368b3f5a --- /dev/null +++ b/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessCreatedStep.vue @@ -0,0 +1,188 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + + + + + + diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessEncryptionStep.vue b/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessEncryptionStep.vue index c096ec68b..6d2d308d5 100644 --- a/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessEncryptionStep.vue +++ b/web/satellite/src/components/accessGrants/newCreateFlow/steps/AccessEncryptionStep.vue @@ -77,6 +77,7 @@ font-size="14px" :on-press="onBack" :is-white="true" + :is-disabled="isLoading" /> @@ -115,6 +117,7 @@ const props = defineProps<{ setOption: (option: PassphraseOption) => void; onBack: () => void; onContinue: () => void; + isLoading: boolean; }>(); const store = useStore(); diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/steps/ChoosePermissionStep.vue b/web/satellite/src/components/accessGrants/newCreateFlow/steps/ChoosePermissionStep.vue index 0d575b63d..b7aa820a4 100644 --- a/web/satellite/src/components/accessGrants/newCreateFlow/steps/ChoosePermissionStep.vue +++ b/web/satellite/src/components/accessGrants/newCreateFlow/steps/ChoosePermissionStep.vue @@ -102,6 +102,7 @@ font-size="14px" :on-press="onBack" :is-white="true" + :is-disabled="isLoading" /> @@ -126,7 +127,8 @@ import { FunctionalContainer, Permission, } from '@/types/createAccessGrant'; -import { useStore } from '@/utils/hooks'; +import { useNotify, useStore } from '@/utils/hooks'; +import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames'; import ContainerWithIcon from '@/components/accessGrants/newCreateFlow/components/ContainerWithIcon.vue'; import ButtonsContainer from '@/components/accessGrants/newCreateFlow/components/ButtonsContainer.vue'; @@ -149,12 +151,14 @@ const props = withDefaults(defineProps<{ notAfterLabel: string; onBack: () => void; onContinue: () => void; + isLoading: boolean; notAfter?: Date; }>(), { notAfter: undefined, }); const store = useStore(); +const notify = useNotify(); const allPermissionsShown = ref(false); const searchBucketsShown = ref(false); @@ -188,6 +192,23 @@ function selectBucket(bucket: string): void { searchQuery.value = ''; } +/** + * Handles continue button click. + */ +function handleContinue(): void { + if (props.notAfter) { + const now = new Date(); + now.setHours(0, 0, 0, 0); + + if (props.notAfter.getTime() < now.getTime()) { + notify.error('End date must be later or equal to today.', AnalyticsErrorEventSource.CREATE_AG_MODAL); + return; + } + } + + props.onContinue(); +} + /** * Selects all buckets and clears search. */ diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/steps/EnterPassphraseStep.vue b/web/satellite/src/components/accessGrants/newCreateFlow/steps/EnterPassphraseStep.vue index 4921a43fb..2b44ff63b 100644 --- a/web/satellite/src/components/accessGrants/newCreateFlow/steps/EnterPassphraseStep.vue +++ b/web/satellite/src/components/accessGrants/newCreateFlow/steps/EnterPassphraseStep.vue @@ -29,6 +29,7 @@ font-size="14px" :on-press="onBack" :is-white="true" + :is-disabled="isLoading" /> @@ -62,6 +63,7 @@ const props = defineProps<{ setPassphrase: (value: string) => void; onBack: () => void; onContinue: () => void; + isLoading: boolean; }>(); const store = useStore(); diff --git a/web/satellite/src/components/accessGrants/newCreateFlow/steps/PassphraseGeneratedStep.vue b/web/satellite/src/components/accessGrants/newCreateFlow/steps/PassphraseGeneratedStep.vue index 2c729dad4..b99f94b32 100644 --- a/web/satellite/src/components/accessGrants/newCreateFlow/steps/PassphraseGeneratedStep.vue +++ b/web/satellite/src/components/accessGrants/newCreateFlow/steps/PassphraseGeneratedStep.vue @@ -57,6 +57,7 @@ font-size="14px" :on-press="onBack" :is-white="true" + :is-disabled="isLoading" /> @@ -91,6 +92,7 @@ const props = defineProps<{ passphrase: string; onBack: () => void; onContinue: () => void; + isLoading: boolean; }>(); const store = useStore(); diff --git a/web/satellite/src/components/modals/EnterPassphraseModal.vue b/web/satellite/src/components/modals/EnterPassphraseModal.vue index 84628b6d7..7b295c578 100644 --- a/web/satellite/src/components/modals/EnterPassphraseModal.vue +++ b/web/satellite/src/components/modals/EnterPassphraseModal.vue @@ -161,7 +161,8 @@ export default class EnterPassphraseModal extends Vue { const gatewayCredentials: EdgeCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, { accessGrant }); await this.$store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials); await this.$store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT); - await this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); + this.$store.commit(OBJECTS_MUTATIONS.SET_PASSPHRASE, this.passphrase); + this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); } /** diff --git a/web/satellite/src/components/modals/OpenBucketModal.vue b/web/satellite/src/components/modals/OpenBucketModal.vue index 4bd86f4d8..cb0f62db3 100644 --- a/web/satellite/src/components/modals/OpenBucketModal.vue +++ b/web/satellite/src/components/modals/OpenBucketModal.vue @@ -108,7 +108,8 @@ export default class OpenBucketModal extends Vue { if (this.isLoading) return; if (this.isWarningState) { - await this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); + this.$store.commit(OBJECTS_MUTATIONS.SET_PASSPHRASE, this.passphrase); + this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); this.closeModal(); this.analytics.pageVisit(RouteConfig.Buckets.with(RouteConfig.UploadFile).path); @@ -134,7 +135,8 @@ export default class OpenBucketModal extends Vue { this.isLoading = false; return; } - await this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); + this.$store.commit(OBJECTS_MUTATIONS.SET_PASSPHRASE, this.passphrase); + this.$store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); this.isLoading = false; this.closeModal(); diff --git a/web/satellite/src/components/modals/createProjectPassphrase/CreateProjectPassphraseModal.vue b/web/satellite/src/components/modals/createProjectPassphrase/CreateProjectPassphraseModal.vue index 099acf001..e00664df7 100644 --- a/web/satellite/src/components/modals/createProjectPassphrase/CreateProjectPassphraseModal.vue +++ b/web/satellite/src/components/modals/createProjectPassphrase/CreateProjectPassphraseModal.vue @@ -247,7 +247,8 @@ async function setAccess(): Promise { const gatewayCredentials: EdgeCredentials = await store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, { accessGrant }); await store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials); await store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT); - await store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); + store.commit(OBJECTS_MUTATIONS.SET_PASSPHRASE, passphrase.value); + store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); } /** diff --git a/web/satellite/src/components/modals/manageProjectPassphrase/SwitchStep.vue b/web/satellite/src/components/modals/manageProjectPassphrase/SwitchStep.vue index f37742a2a..9493ba145 100644 --- a/web/satellite/src/components/modals/manageProjectPassphrase/SwitchStep.vue +++ b/web/satellite/src/components/modals/manageProjectPassphrase/SwitchStep.vue @@ -171,6 +171,7 @@ async function setAccess(): Promise { const gatewayCredentials: EdgeCredentials = await store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, { accessGrant }); await store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials); await store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT); + store.commit(OBJECTS_MUTATIONS.SET_PASSPHRASE, passphrase.value); store.commit(OBJECTS_MUTATIONS.SET_PROMPT_FOR_PASSPHRASE, false); } diff --git a/web/satellite/src/types/createAccessGrant.ts b/web/satellite/src/types/createAccessGrant.ts index aeba70f50..fdab1c39d 100644 --- a/web/satellite/src/types/createAccessGrant.ts +++ b/web/satellite/src/types/createAccessGrant.ts @@ -8,6 +8,7 @@ import ChoosePermissionIcon from '@/../static/images/accessGrants/newCreateFlow/ import AccessEncryptionIcon from '@/../static/images/accessGrants/newCreateFlow/accessEncryption.svg'; import PassphraseGeneratedIcon from '@/../static/images/accessGrants/newCreateFlow/passphraseGenerated.svg'; import AccessCreatedIcon from '@/../static/images/accessGrants/newCreateFlow/accessCreated.svg'; +import CLIAccessCreatedIcon from '@/../static/images/accessGrants/newCreateFlow/cliAccessCreated.svg'; import CredentialsCreatedIcon from '@/../static/images/accessGrants/newCreateFlow/credentialsCreated.svg'; import EncryptionInfoIcon from '@/../static/images/accessGrants/newCreateFlow/encryptionInfo.svg'; import TypeIcon from '@/../static/images/accessGrants/newCreateFlow/typeIcon.svg'; @@ -44,6 +45,7 @@ export enum CreateAccessStep { EnterMyPassphrase = 'enterMyPassphrase', EnterNewPassphrase = 'enterNewPassphrase', AccessCreated = 'accessCreated', + CLIAccessCreated = 'cliAccessCreated', CredentialsCreated = 'credentialsCreated', } @@ -92,6 +94,10 @@ export const STEP_ICON_AND_TITLE: Record = { icon: CredentialsCreatedIcon, title: 'Credentials created', }, + [CreateAccessStep.CLIAccessCreated]: { + icon: CLIAccessCreatedIcon, + title: 'CLI access created', + }, }; export enum FunctionalContainer { diff --git a/web/satellite/static/images/accessGrants/newCreateFlow/cliAccessCreated.svg b/web/satellite/static/images/accessGrants/newCreateFlow/cliAccessCreated.svg new file mode 100644 index 000000000..e4be45761 --- /dev/null +++ b/web/satellite/static/images/accessGrants/newCreateFlow/cliAccessCreated.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +