web/satellite: moved object's passphrase step to be after buckets

Moved passphrase step to be after buckets screen.
Seems like it accidently enabled bucket force delete feature because buckets AG is being generated with empty passphrase.

Change-Id: I7ca0daf4e49045bf4d49b996eb5e2406132caeea
This commit is contained in:
Vitalii Shpital 2021-11-02 19:04:30 +02:00
parent 3c683998f5
commit 696b8f0d8e
10 changed files with 222 additions and 42 deletions

View File

@ -70,11 +70,11 @@ func TestNavigation(t *testing.T) {
dashboardTitle1 := page.MustElement("[aria-roledescription=title]").MustText()
require.Contains(t, dashboardTitle1, "Dashboard")
// project dashboard route
// objects route
page.MustElementR("p", "Objects").MustClick()
waitVueTick(page)
objectsTitle := page.MustElement("[aria-roledescription=enc-title]").MustText()
require.Contains(t, objectsTitle, "Encryption passphrase")
objectsTitle := page.MustElement("[aria-roledescription=title]").MustText()
require.Contains(t, objectsTitle, "Buckets")
// access grants route
page.MustElementR("p", "Access").MustClick()
@ -124,8 +124,8 @@ func TestNavigation(t *testing.T) {
page.MustElementR("p", "Quick Start").MustClick()
page.MustElement("[aria-roledescription=objects-route]").MustClick()
waitVueTick(page)
objectsTitle1 := page.MustElement("[aria-roledescription=enc-title]").MustText()
require.Contains(t, objectsTitle1, "Encryption passphrase")
objectsTitle1 := page.MustElement("[aria-roledescription=title]").MustText()
require.Contains(t, objectsTitle1, "Buckets")
// onboarding cli flow route
page.MustElementR("p", "Quick Start").MustClick()

View File

@ -11,12 +11,11 @@ import (
"github.com/stretchr/testify/require"
"storj.io/common/testcontext"
"storj.io/storj/private/testplanet"
"storj.io/storj/testsuite/ui/uitest"
)
func TestOnboardingWizardBrowser(t *testing.T) {
uitest.Run(t, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet, browser *rod.Browser) {
uitest.Edge(t, func(t *testing.T, ctx *testcontext.Context, planet *uitest.EdgePlanet, browser *rod.Browser) {
signupPageURL := planet.Satellites[0].ConsoleURL() + "/signup"
fullName := "John Doe"
emailAddress := "test@email.com"
@ -44,28 +43,33 @@ func TestOnboardingWizardBrowser(t *testing.T) {
waitVueTick(page)
// testing onboarding workflow browser
wait := page.MustWaitRequestIdle()
page.MustElementX("(//span[text()=\"Continue in web\"])").MustClick()
waitVueTick(page)
wait()
objectBrowserWarning := page.MustElement("[aria-roledescription=objects-title]").MustText()
require.Contains(t, objectBrowserWarning, "The object browser uses server side encryption.")
// Buckets Page
bucketsTitle := page.MustElement("[aria-roledescription=title]").MustText()
require.Contains(t, bucketsTitle, "Buckets")
page.Race().ElementR("p", "demo-bucket").MustHandle(func(el *rod.Element) {
el.MustClick()
waitVueTick(page)
}).MustDo()
encryptionPassphraseWarningTitle := page.MustElement("[aria-roledescription=warning-title]").MustText()
require.Contains(t, encryptionPassphraseWarningTitle, "Save your encryption passphrase")
// Passphrase screen
encryptionTitle := page.MustElement("[aria-roledescription=objects-title]").MustText()
require.Contains(t, encryptionTitle, "The object browser uses server side encryption.")
customPassphrase := page.MustElement("[aria-roledescription=enter-passphrase-label]")
customPassphraseLabel := customPassphrase.MustText()
require.Contains(t, customPassphraseLabel, "Enter your own passphrase")
customPassphrase.MustClick()
waitVueTick(page)
page.MustElement("[aria-roledescription=passphrase] input").MustInput("password123")
page.MustElement(".checkmark").MustClick()
waitVueTick(page)
page.MustElementX("(//span[text()=\"Next >\"])").MustClick()
waitVueTick(page)
// Buckets Page
bucketsTitle := page.MustElement("[aria-roledescription=title]").MustText()
require.Contains(t, bucketsTitle, "Buckets")
// Verify that browser component has loaded and that the dropzone is present
page.MustElementR("h4", "Drop Files Here to Upload")
})
}

View File

@ -72,6 +72,7 @@ func Edge(t *testing.T, test EdgeTest) {
config.Console.StaticDir = dir
}
config.Console.NewOnboarding = true
config.Console.NewObjectsFlow = true
// TODO: this should be dynamically set from the auth service
config.Console.GatewayCredentialsRequestURL = "http://" + authSvcAddr
},

View File

@ -42,6 +42,7 @@ func Run(t *testing.T, test Test) {
config.Console.NewOnboarding = true
config.Console.NewNavigation = true
config.Console.CouponCodeBillingUIEnabled = true
config.Console.NewObjectsFlow = true
},
},
NonParallel: true,

View File

@ -54,6 +54,7 @@
:error="enterError"
role-description="passphrase"
is-password="true"
:disabled="isLoading"
@setData="setPassphrase"
/>
</div>
@ -73,6 +74,16 @@
/>
</div>
<div class="encrypt-container__buttons">
<VButton
v-if="isNewObjectsFlow"
class="encrypt-container__buttons__back"
label="< Back"
height="64px"
border-radius="62px"
is-blue-white="true"
:on-press="onBackClick"
:is-disabled="isLoading"
/>
<VButton
label="Next >"
height="64px"
@ -114,6 +125,8 @@ export default class GeneratePassphrase extends Vue {
@Prop({ default: () => null })
public readonly onNextClick: () => unknown;
@Prop({ default: () => null })
public readonly onBackClick: () => unknown;
@Prop({ default: () => null })
public readonly setParentPassphrase: (passphrase: string) => void;
@Prop({ default: false })
public readonly isLoading: boolean;
@ -217,6 +230,13 @@ export default class GeneratePassphrase extends Vue {
await this.onNextClick();
}
/**
* Returns objects flow status from store.
*/
public get isNewObjectsFlow(): string {
return this.$store.state.appStateModule.isNewObjectsFlow;
}
}
</script>
@ -366,6 +386,10 @@ export default class GeneratePassphrase extends Vue {
display: flex;
align-items: center;
margin-top: 30px;
&__back {
margin-right: 24px;
}
}
}

View File

@ -88,7 +88,6 @@ export default class BucketsView extends Vue {
private readonly FILE_BROWSER_AG_NAME: string = 'Web file browser API key';
private worker: Worker;
private grantWithPermissions = '';
private accessGrant = '';
private createBucketName = '';
private deleteBucketName = '';
@ -104,7 +103,7 @@ export default class BucketsView extends Vue {
* Setup gateway credentials.
*/
public async mounted(): Promise<void> {
if (!this.$store.state.objectsModule.passphrase) {
if (!this.$store.state.objectsModule.passphrase && !this.isNewObjectsFlow) {
await this.$router.push(RouteConfig.Objects.with(RouteConfig.EncryptData).path);
return;
@ -152,23 +151,26 @@ export default class BucketsView extends Vue {
}
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
let passphrase = '';
if (!this.isNewObjectsFlow) {
passphrase = this.passphrase;
}
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': this.grantWithPermissions,
'passphrase': this.passphrase,
'passphrase': passphrase,
'projectID': this.$store.getters.selectedProject.id,
'satelliteNodeURL': satelliteNodeURL,
});
const accessGrantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
this.accessGrant = accessGrantEvent.data.value;
if (accessGrantEvent.data.error) {
throw new Error(accessGrantEvent.data.error);
}
await this.$store.dispatch(OBJECTS_ACTIONS.SET_ACCESS_GRANT, this.accessGrant);
const accessGrant = accessGrantEvent.data.value;
const gatewayCredentials: GatewayCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, {accessGrant: this.accessGrant});
const gatewayCredentials: GatewayCredentials = 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);
}
@ -202,6 +204,14 @@ export default class BucketsView extends Vue {
try {
await this.$store.dispatch(OBJECTS_ACTIONS.CREATE_BUCKET, this.createBucketName);
if (this.isNewObjectsFlow) {
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
this.createBucketName = '';
this.isRequestProcessing = false;
this.hideCreateBucketPopup();
return;
}
} catch (error) {
const BUCKET_ALREADY_EXISTS_ERROR = 'BucketAlreadyExists';
@ -233,6 +243,12 @@ export default class BucketsView extends Vue {
try {
await this.$store.dispatch(OBJECTS_ACTIONS.CREATE_DEMO_BUCKET);
if (this.isNewObjectsFlow) {
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
this.isRequestProcessing = false;
return;
}
} catch (error) {
await this.$notify.error(error.message);
this.isRequestProcessing = false;
@ -345,6 +361,13 @@ export default class BucketsView extends Vue {
*/
public openBucket(bucketName: string): void {
this.$store.dispatch(OBJECTS_ACTIONS.SET_FILE_COMPONENT_BUCKET_NAME, bucketName);
if (this.isNewObjectsFlow) {
this.$router.push(RouteConfig.Objects.with(RouteConfig.EncryptData).path);
return;
}
this.$router.push(RouteConfig.Objects.with(RouteConfig.UploadFile).path);
}
@ -362,6 +385,13 @@ export default class BucketsView extends Vue {
return this.$store.state.objectsModule.passphrase;
}
/**
* Returns objects flow status from store.
*/
private get isNewObjectsFlow(): string {
return this.$store.state.appStateModule.isNewObjectsFlow;
}
/**
* Returns validation status of a bucket name.
*/

View File

@ -22,6 +22,7 @@
</div>
<GeneratePassphrase
:on-next-click="onNextClick"
:on-back-click="onBackClick"
:set-parent-passphrase="setPassphrase"
:is-loading="isLoading"
/>
@ -63,6 +64,9 @@ import pbkdf2 from 'pbkdf2';
import { RouteConfig } from '@/router';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { LocalData } from "@/utils/localData";
import { GatewayCredentials } from "@/types/accessGrants";
import { ACCESS_GRANTS_ACTIONS } from "@/store/modules/accessGrants";
import { MetaUtils } from "@/utils/meta";
import GeneratePassphrase from "@/components/common/GeneratePassphrase.vue";
import FAQBullet from "@/components/objects/FAQBullet.vue";
@ -75,6 +79,8 @@ import FAQBullet from "@/components/objects/FAQBullet.vue";
},
})
export default class EncryptData extends Vue {
private worker: Worker;
public isLoading = false;
public passphrase = '';
@ -90,6 +96,20 @@ export default class EncryptData extends Vue {
})
}
/**
* Lifecycle hook after initial render.
* Sets local worker if new flow is used.
*/
public mounted(): void {
if (this.isNewObjectsFlow) {
if (!this.apiKey) {
this.$router.push(RouteConfig.Objects.with(RouteConfig.BucketsManagement).path)
}
this.setWorker();
}
}
/**
* Sets passphrase from child component.
*/
@ -97,6 +117,16 @@ export default class EncryptData extends Vue {
this.passphrase = passphrase;
}
/**
* Sets local worker with worker instantiated in store.
*/
public setWorker(): void {
this.worker = this.$store.state.accessGrantsModule.accessGrantsWebWorker;
this.worker.onerror = (error: ErrorEvent) => {
this.$notify.error(error.message);
};
}
/**
* Holds on next button click logic.
*/
@ -120,9 +150,72 @@ export default class EncryptData extends Vue {
await LocalData.setUserIDPassSalt(this.$store.getters.user.id, keyToBeStored, SALT);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
if (this.isNewObjectsFlow) {
try {
await this.setAccess();
await this.$router.push(RouteConfig.UploadFile.path);
} catch (e) {
await this.$notify.error(e.message);
}
this.isLoading = false;
return;
}
this.isLoading = false;
await this.$router.push({name: RouteConfig.BucketsManagement.name});
await this.$router.push(RouteConfig.BucketsManagement.path);
}
/**
* Holds on back button click logic.
*/
public onBackClick(): void {
this.$router.push(RouteConfig.BucketsManagement.path);
}
/**
* Sets access to S3 client.
*/
public async setAccess(): Promise<void> {
const now = new Date();
const inThreeDays = new Date(now.setDate(now.getDate() + 3));
await this.worker.postMessage({
'type': 'SetPermission',
'isDownload': true,
'isUpload': true,
'isList': true,
'isDelete': true,
'notAfter': inThreeDays.toISOString(),
'buckets': this.bucket ? [this.bucket] : [],
'apiKey': this.apiKey,
});
const grantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
if (grantEvent.data.error) {
throw new Error(grantEvent.data.error);
}
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': grantEvent.data.value,
'passphrase': this.passphrase,
'projectID': this.$store.getters.selectedProject.id,
'satelliteNodeURL': satelliteNodeURL,
});
const accessGrantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
if (accessGrantEvent.data.error) {
throw new Error(accessGrantEvent.data.error);
}
const accessGrant = accessGrantEvent.data.value;
const gatewayCredentials: GatewayCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, {accessGrant});
await this.$store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials);
}
/**
@ -138,6 +231,27 @@ export default class EncryptData extends Vue {
});
});
}
/**
* Returns apiKey from store.
*/
private get apiKey(): string {
return this.$store.state.objectsModule.apiKey;
}
/**
* Returns bucket name from store.
*/
private get bucket(): string {
return this.$store.state.objectsModule.fileComponentBucketName;
}
/**
* Returns objects flow status from store.
*/
private get isNewObjectsFlow(): string {
return this.$store.state.appStateModule.isNewObjectsFlow;
}
}
</script>

View File

@ -47,7 +47,11 @@ export default class UploadFile extends Vue {
*/
public mounted(): void {
if (!this.bucket) {
this.$router.push(RouteConfig.Objects.with(RouteConfig.EncryptData).path);
if (this.isNewObjectsFlow) {
this.$router.push(RouteConfig.Objects.with(RouteConfig.BucketsManagement).path);
} else {
this.$router.push(RouteConfig.Objects.with(RouteConfig.EncryptData).path);
}
return;
}
@ -133,13 +137,6 @@ export default class UploadFile extends Vue {
};
}
/**
* Indicates if upload cancel popup is visible.
*/
public get isCancelUploadPopupVisible(): boolean {
return this.$store.state.appStateModule.appState.isUploadCancelPopupVisible;
}
/**
* Generates public access key.
*/
@ -185,6 +182,20 @@ export default class UploadFile extends Vue {
return gatewayCredentials.accessKeyId;
}
/**
* Indicates if upload cancel popup is visible.
*/
public get isCancelUploadPopupVisible(): boolean {
return this.$store.state.appStateModule.appState.isUploadCancelPopupVisible;
}
/**
* Returns objects flow status from store.
*/
private get isNewObjectsFlow(): string {
return this.$store.state.appStateModule.isNewObjectsFlow;
}
/**
* Returns passphrase from store.
*/

View File

@ -493,6 +493,12 @@ router.beforeEach(async (to, from, next) => {
}
if (navigateToDefaultSubTab(to.matched, RouteConfig.Objects)) {
if (store.state.appStateModule.isNewObjectsFlow) {
next(RouteConfig.Objects.with(RouteConfig.BucketsManagement).path);
return;
}
next(RouteConfig.Objects.with(RouteConfig.EncryptData).path);
return;

View File

@ -11,7 +11,6 @@ export const OBJECTS_ACTIONS = {
CLEAR: 'clearObjects',
SET_GATEWAY_CREDENTIALS: 'setGatewayCredentials',
SET_API_KEY: 'setApiKey',
SET_ACCESS_GRANT: 'setAccessGrant',
SET_S3_CLIENT: 'setS3Client',
SET_PASSPHRASE: 'setPassphrase',
SET_FILE_COMPONENT_BUCKET_NAME: 'setFileComponentBucketName',
@ -25,7 +24,6 @@ export const OBJECTS_ACTIONS = {
export const OBJECTS_MUTATIONS = {
SET_GATEWAY_CREDENTIALS: 'setGatewayCredentials',
SET_API_KEY: 'setApiKey',
SET_ACCESS_GRANT: 'setAccessGrant',
CLEAR: 'clearObjects',
SET_S3_CLIENT: 'setS3Client',
SET_BUCKETS: 'setBuckets',
@ -39,7 +37,6 @@ export const DEMO_BUCKET_NAME = 'demo-bucket';
const {
CLEAR,
SET_API_KEY,
SET_ACCESS_GRANT,
SET_GATEWAY_CREDENTIALS,
SET_S3_CLIENT,
SET_BUCKETS,
@ -50,7 +47,6 @@ const {
export class ObjectsState {
public apiKey = '';
public accessGrant = '';
public gatewayCredentials: GatewayCredentials = new GatewayCredentials();
public s3Client: S3 = new S3({
s3ForcePathStyle: true,
@ -84,9 +80,6 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
[SET_API_KEY](state: ObjectsState, apiKey: string) {
state.apiKey = apiKey;
},
[SET_ACCESS_GRANT](state: ObjectsState, accessGrant: string) {
state.accessGrant = accessGrant;
},
[SET_GATEWAY_CREDENTIALS](state: ObjectsState, credentials: GatewayCredentials) {
state.gatewayCredentials = credentials;
},
@ -117,7 +110,6 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
[CLEAR](state: ObjectsState) {
state.apiKey = '';
state.passphrase = '';
state.accessGrant = '';
state.gatewayCredentials = new GatewayCredentials();
state.s3Client = new S3({
s3ForcePathStyle: true,
@ -132,9 +124,6 @@ export function makeObjectsModule(): StoreModule<ObjectsState, ObjectsContext> {
setApiKey: function({commit}: ObjectsContext, apiKey: string): void {
commit(SET_API_KEY, apiKey);
},
setAccessGrant: function({commit}: ObjectsContext, accessGrant: string): void {
commit(SET_ACCESS_GRANT, accessGrant);
},
setGatewayCredentials: function({commit}: ObjectsContext, credentials: GatewayCredentials): void {
commit(SET_GATEWAY_CREDENTIALS, credentials);
},