Skip to content

Commit

Permalink
web/satellite: prompt for project passphrase
Browse files Browse the repository at this point in the history
This change prompts a user for passphrase when they open or change
projects. It allows them to skip this step permanently.

Issue: #6778

Change-Id: Ib851c4072561c8483a5291a441e492f54970c277
  • Loading branch information
wilfred-asomanii authored and Storj Robot committed Feb 23, 2024
1 parent 41e63b5 commit 7a574ec
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 2 deletions.
21 changes: 19 additions & 2 deletions web/satellite/src/App.vue
Expand Up @@ -9,7 +9,7 @@
</template>

<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useTheme } from 'vuetify';
import { useRoute, useRouter } from 'vue-router';
Expand Down Expand Up @@ -84,12 +84,17 @@ async function setup() {
await projectsStore.createDefaultProject(usersStore.state.user.id);
}
projectsStore.selectProject(projects[0].id);
const project = projectsStore.state.selectedProject;
await router.push({
name: ROUTES.Dashboard.name,
params: { id: projectsStore.state.selectedProject.urlId },
params: { id: project.urlId },
});
analyticsStore.pageVisit(ROUTES.DashboardAnalyticsLink);
analyticsStore.eventTriggered(AnalyticsEvent.NAVIGATE_PROJECTS);
if (usersStore.getShouldPromptPassphrase(project.ownerId === usersStore.state.user.id)) {
appStore.toggleProjectPassphraseDialog(true);
}
}
} catch (error) {
if (!(error instanceof ErrorUnauthorized)) {
Expand Down Expand Up @@ -132,4 +137,16 @@ usersStore.$onAction(({ name, after }) => {
after((_) => setup());
}
});
/**
* conditionally prompt for project passphrase if project changes
*/
watch(() => projectsStore.state.selectedProject, (project, oldProject) => {
if (project.id === oldProject.id) {
return;
}
if (usersStore.getShouldPromptPassphrase(project.ownerId === usersStore.state.user.id)) {
appStore.toggleProjectPassphraseDialog(true);
}
});
</script>
1 change: 1 addition & 0 deletions web/satellite/src/components/ProjectsTableComponent.vue
Expand Up @@ -163,6 +163,7 @@ const decliningIds = ref(new Set<string>());
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
Expand Down
Expand Up @@ -53,6 +53,7 @@
<v-row>
<v-col>
<v-btn
:disabled="isLoading"
variant="outlined"
color="default"
block
Expand Down
Expand Up @@ -4,6 +4,7 @@
<template>
<v-dialog
v-model="model"
:persistent="isLoading"
width="400px"
transition="fade-transition"
>
Expand Down
190 changes: 190 additions & 0 deletions web/satellite/src/components/dialogs/EnterProjectPassphraseDialog.vue
@@ -0,0 +1,190 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<v-dialog
v-model="model"
:persistent="isLoading"
width="400px"
transition="fade-transition"
>
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="pa-5 pl-7">
<template #prepend>
<img class="d-block" src="@/assets/createAccessGrantFlow/accessEncryption.svg" alt="icon">
</template>

<v-card-title class="font-weight-bold">
{{ isSkipping ? 'Skip passphrase' : '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="pa-7 pb-3">
<v-form v-model="formValid" @submit.prevent="onContinue">
<v-row>
<v-col cols="12">
<p v-if="isSkipping" class="pb-3">
Do you want to remember this choice and always skip the passphrase when opening a project?
</p>
<p v-else>
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 v-if="!isSkipping" cols="12">
<v-text-field
id="Encryption Passphrase"
v-model="passphrase"
label="Encryption Passphrase"
:type="isPassphraseVisible ? 'text' : 'password'"
variant="outlined"
:hide-details="false"
:rules="[ RequiredRule ]"
autofocus
>
<template #append-inner>
<password-input-eye-icons
:is-visible="isPassphraseVisible"
type="passphrase"
@toggleVisibility="isPassphraseVisible = !isPassphraseVisible"
/>
</template>
</v-text-field>
</v-col>
</v-row>
</v-form>
</v-card-item>

<v-divider />

<v-card-actions class="pa-4">
<v-col>
<v-btn
variant="outlined"
color="default"
block
:disabled="isLoading"
@click="() => isSkipping ? model = false : onSkip()"
>
{{ isSkipping ? 'No' : 'Skip' }}
</v-btn>
</v-col>
<v-col>
<v-btn
color="primary"
variant="flat"
block
:loading="isLoading"
:disabled="!formValid"
@click="() => isSkipping ? onSkip(true) : onContinue()"
>
{{ isSkipping ? 'Yes' : '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 {
VBtn,
VCard,
VCardActions,
VCardItem,
VCardTitle,
VCol,
VDialog,
VDivider,
VForm,
VRow,
VTextField,
} from 'vuetify/components';
import { RequiredRule } from '@/types/common';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useLoading } from '@/composables/useLoading';
import {
AnalyticsErrorEventSource,
AnalyticsEvent,
} from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useUsersStore } from '@/store/modules/usersStore.js';
import PasswordInputEyeIcons from '@/components/PasswordInputEyeIcons.vue';
const analyticsStore = useAnalyticsStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const projectsStore = useProjectsStore();
const usersStore = useUsersStore();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const passphrase = ref<string>('');
const isPassphraseVisible = ref<boolean>(false);
const isSkipping = ref<boolean>(false);
const innerContent = ref<Component | null>(null);
const formValid = ref<boolean>(false);
const model = computed({
get: () => appStore.state.isProjectPassphraseDialogShown,
set: appStore.toggleProjectPassphraseDialog,
});
const emit = defineEmits<{
(event: 'passphraseEntered'): void,
}>();
function onSkip(confirmed = false): void {
if (!confirmed) {
isSkipping.value = true;
return;
}
withLoading(async () => {
try {
await usersStore.updateSettings({ passphrasePrompt: false });
model.value = false;
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.CREATE_PROJECT_PASSPHRASE_MODAL);
}
});
}
async function onContinue(): Promise<void> {
analyticsStore.eventTriggered(AnalyticsEvent.PASSPHRASE_CREATED, {
method: 'enter',
});
bucketsStore.setPassphrase(passphrase.value);
bucketsStore.setPromptForPassphrase(false);
model.value = false;
}
watch(innerContent, comp => {
if (!comp) {
passphrase.value = '';
}
});
</script>
Expand Up @@ -118,6 +118,10 @@ const props = defineProps<{
const model = defineModel<boolean>({ required: true });
const emit = defineEmits<{
(event: 'deleted'): void;
}>();
const analyticsStore = useAnalyticsStore();
const configStore = useConfigStore();
const projectsStore = useProjectsStore();
Expand All @@ -133,6 +137,7 @@ async function onDelete(): Promise<void> {
try {
await pmStore.deleteProjectMembers(projectsStore.state.selectedProject.id, props.emails);
notify.success('Members were successfully removed from the project');
emit('deleted');
model.value = false;
} catch (error) {
error.message = `Error removing project members. ${error.message}`;
Expand Down
3 changes: 3 additions & 0 deletions web/satellite/src/layouts/default/ProjectNav.vue
Expand Up @@ -239,6 +239,7 @@

<create-project-dialog v-model="isCreateProjectDialogShown" />
<manage-passphrase-dialog v-model="isManagePassphraseDialogShown" />
<enter-project-passphrase-dialog />
</template>

<script setup lang="ts">
Expand Down Expand Up @@ -284,6 +285,8 @@ import ManagePassphraseDialog from '@/components/dialogs/ManagePassphraseDialog.
import NavigationItem from '@/layouts/default/NavigationItem.vue';
import IconFolder from '@/components/icons/IconFolder.vue';
import IconApplications from '@/components/icons/IconApplications.vue';
import EnterProjectPassphraseDialog
from '@/components/dialogs/EnterProjectPassphraseDialog.vue';
const configStore = useConfigStore();
const analyticsStore = useAnalyticsStore();
Expand Down
6 changes: 6 additions & 0 deletions web/satellite/src/store/modules/appStore.ts
Expand Up @@ -15,6 +15,7 @@ class AppState {
public isNavigationDrawerShown = true;
public isUpgradeFlowDialogShown = false;
public isAccountSetupDialogShown = false;
public isProjectPassphraseDialogShown = false;
public pathBeforeAccountPage: string | null = null;
public isNavigating = false;
}
Expand Down Expand Up @@ -76,6 +77,10 @@ export const useAppStore = defineStore('app', () => {
state.isAccountSetupDialogShown = isShown ?? !state.isAccountSetupDialogShown;
}

function toggleProjectPassphraseDialog(isShown?: boolean): void {
state.isProjectPassphraseDialogShown = isShown ?? !state.isProjectPassphraseDialogShown;
}

function setPathBeforeAccountPage(path: string) {
state.pathBeforeAccountPage = path;
}
Expand Down Expand Up @@ -110,6 +115,7 @@ export const useAppStore = defineStore('app', () => {
toggleBrowserCardViewEnabled: toggleBrowserTableViewEnabled,
hasProjectTableViewConfigured,
toggleHasJustLoggedIn,
toggleProjectPassphraseDialog,
setUploadingModal,
setErrorPage,
removeErrorPage,
Expand Down
15 changes: 15 additions & 0 deletions web/satellite/src/store/modules/usersStore.ts
Expand Up @@ -5,7 +5,9 @@ import { defineStore } from 'pinia';
import { computed, DeepReadonly, reactive, readonly } from 'vue';

import {
ACCOUNT_SETUP_STEPS,
DisableMFARequest,
OnboardingStep,
SetUserSettingsData,
UpdatedUser,
User,
Expand Down Expand Up @@ -56,6 +58,18 @@ export const useUsersStore = defineStore('users', () => {
setUser(user);
}

function getShouldPromptPassphrase(isOwner: boolean): boolean {
const settings = state.settings;
const step = settings.onboardingStep as OnboardingStep || OnboardingStep.AccountTypeSelection;
if (settings.onboardingEnd || !settings.passphrasePrompt) {
return settings.passphrasePrompt;
}
if (!isOwner) {
return !ACCOUNT_SETUP_STEPS.includes(step);
}
return step !== OnboardingStep.EncryptionPassphrase && !ACCOUNT_SETUP_STEPS.includes(step);
}

async function disableUserMFA(request: DisableMFARequest): Promise<void> {
await api.disableUserMFA(request.passcode, request.recoveryCode);
}
Expand Down Expand Up @@ -124,6 +138,7 @@ export const useUsersStore = defineStore('users', () => {
generateUserMFASecret,
generateUserMFARecoveryCodes,
regenerateUserMFARecoveryCodes,
getShouldPromptPassphrase,
clear,
login,
setUser,
Expand Down

0 comments on commit 7a574ec

Please sign in to comment.