web/satellite: cli flow's Encrypt Your Data component

Refactored Generate Passphrase screen to be common for onb and object flows.
Replaced initial Generate Passphrase screen to access grants wizard.
Moved download file functionality to utils.
Implemented Onboarding Encrypt your Data logic.

Change-Id: I46e780a5997c215d53ccd0254964b1c2b39c7c02
This commit is contained in:
Vitalii Shpital 2021-08-30 17:15:43 +03:00
parent 7a08c19c53
commit 390c6b6fc6
12 changed files with 828 additions and 605 deletions

View File

@ -4,29 +4,107 @@
<template>
<div class="create-passphrase">
<BackIcon class="create-passphrase__back-icon" @click="onBackClick" />
<GeneratePassphrase
:is-loading="isLoading"
:on-button-click="onNextClick"
:set-parent-passphrase="setPassphrase"
/>
<div class="create-passphrase__container">
<h1 class="create-passphrase__container__title">Encryption Passphrase</h1>
<div class="create-passphrase__container__choosing">
<p class="create-passphrase__container__choosing__label">Passphrase</p>
<div class="create-passphrase__container__choosing__right">
<p
class="create-passphrase__container__choosing__right__option left-option"
:class="{ active: isGenerateState }"
@click="onChooseGenerate"
>
Generate Phrase
</p>
<p
class="create-passphrase__container__choosing__right__option"
:class="{ active: isEnterState }"
@click="onChooseCreate"
>
Enter Phrase
</p>
</div>
</div>
<div v-if="isEnterState" class="create-passphrase__container__enter-passphrase-box">
<div class="create-passphrase__container__enter-passphrase-box__header">
<GreenWarningIcon />
<h2 class="create-passphrase__container__enter-passphrase-box__header__label">Enter an Existing Passphrase</h2>
</div>
<p class="create-passphrase__container__enter-passphrase-box__message">
if you already have an encryption passphrase, enter your encryption passphrase here.
</p>
</div>
<div class="create-passphrase__container__value-area">
<div v-if="isGenerateState" class="create-passphrase__container__value-area__mnemonic">
<p class="create-passphrase__container__value-area__mnemonic__value">{{ passphrase }}</p>
<VButton
class="create-passphrase__container__value-area__mnemonic__button"
label="Copy"
width="66px"
height="30px"
:on-press="onCopyClick"
/>
</div>
<div v-else class="create-passphrase__container__value-area__password">
<HeaderedInput
class="create-passphrase__container__value-area__password__input"
placeholder="Enter encryption passphrase here"
:error="errorMessage"
@setData="onChangePassphrase"
/>
</div>
</div>
<div v-if="isGenerateState" class="create-passphrase__container__warning">
<h2 class="create-passphrase__container__warning__title">Save Your Encryption Passphrase</h2>
<p class="create-passphrase__container__warning__message">
Youll need this passphrase to access data in the future. This is the only time it will be displayed.
Be sure to write it down.
</p>
<label class="create-passphrase__container__warning__check-area" :class="{ error: isError }" for="pass-checkbox">
<input
id="pass-checkbox"
v-model="isChecked"
class="create-passphrase__container__warning__check-area__checkbox"
type="checkbox"
@change="isError = false"
>
Yes, I wrote this down or saved it somewhere.
</label>
</div>
<VButton
class="create-passphrase__container__next-button"
label="Next"
width="100%"
height="48px"
:on-press="onNextClick"
:is-disabled="isButtonDisabled"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import GeneratePassphrase from '@/components/common/GeneratePassphrase.vue';
import BackIcon from '@/../static/images/accessGrants/back.svg';
import * as bip39 from "bip39";
import { RouteConfig } from '@/router';
import { MetaUtils } from '@/utils/meta';
import { AnalyticsEvent } from "@/utils/constants/analyticsEventNames";
import { AnalyticsHttpApi } from "@/api/analytics";
import VButton from "@/components/common/VButton.vue";
import HeaderedInput from "@/components/common/HeaderedInput.vue";
import BackIcon from '@/../static/images/accessGrants/back.svg';
import GreenWarningIcon from '@/../static/images/accessGrants/greenWarning.svg';
// @vue/component
@Component({
components: {
BackIcon,
GeneratePassphrase,
GreenWarningIcon,
VButton,
HeaderedInput,
},
})
export default class CreatePassphraseStep extends Vue {
@ -36,7 +114,14 @@ export default class CreatePassphraseStep extends Vue {
private worker: Worker;
private isLoading = true;
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
public isGenerateState = true;
public isEnterState = false;
public isChecked = false;
public isError = false;
public passphrase = '';
public errorMessage = '';
/**
* Lifecycle hook after initial render.
@ -54,6 +139,8 @@ export default class CreatePassphraseStep extends Vue {
this.setWorker();
this.passphrase = bip39.generateMnemonic();
this.isLoading = false;
}
@ -80,10 +167,24 @@ export default class CreatePassphraseStep extends Vue {
* Generates access grant and redirects to next step.
*/
public async onNextClick(): Promise<void> {
if (!this.passphrase) {
this.errorMessage = 'Passphrase can\'t be empty';
return;
}
if (!this.isChecked && this.isGenerateState) {
this.isError = true;
return;
}
if (this.isLoading) return;
this.isLoading = true;
await this.analytics.eventTriggered(AnalyticsEvent.PASSPHRASE_CREATED);
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
@ -117,6 +218,56 @@ export default class CreatePassphraseStep extends Vue {
});
}
/**
* Changes state to generate passphrase.
*/
public onChooseGenerate(): void {
if (this.passphrase && this.isGenerateState) return;
this.passphrase = bip39.generateMnemonic();
this.isEnterState = false;
this.isGenerateState = true;
}
/**
* Changes state to create passphrase.
*/
public onChooseCreate(): void {
if (this.passphrase && this.isEnterState) return;
this.errorMessage = '';
this.passphrase = '';
this.isEnterState = true;
this.isGenerateState = false;
}
/**
* Holds on copy button click logic.
* Copies passphrase to clipboard.
*/
public onCopyClick(): void {
this.$copyText(this.passphrase);
this.$notify.success('Passphrase was copied successfully');
}
/**
* Changes passphrase data from input value.
* @param value
*/
public onChangePassphrase(value: string): void {
this.passphrase = value.trim();
this.errorMessage = '';
}
/**
* Indicates if button is disabled.
*/
public get isButtonDisabled(): boolean {
return this.isLoading || !this.passphrase || (!this.isChecked && this.isGenerateState);
}
/**
* Holds on back button click logic.
* Redirects to previous step.
@ -142,5 +293,202 @@ export default class CreatePassphraseStep extends Vue {
left: 65px;
cursor: pointer;
}
&__container {
padding: 25px 50px;
max-width: 515px;
min-width: 515px;
font-family: 'font_regular', sans-serif;
font-style: normal;
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
border-radius: 6px;
&__title {
font-family: 'font_bold', sans-serif;
font-weight: bold;
font-size: 22px;
line-height: 27px;
color: #000;
margin: 0 0 30px 0;
}
&__enter-passphrase-box {
padding: 20px;
background: #f9fffc;
border: 1px solid #1a9666;
border-radius: 9px;
&__header {
display: flex;
align-items: center;
margin-bottom: 10px;
&__label {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0 0 0 10px;
}
}
&__message {
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0;
}
}
&__warning {
display: flex;
flex-direction: column;
padding: 20px;
width: calc(100% - 40px);
margin: 35px 0;
background: #fff;
border: 1px solid #e6e9ef;
border-radius: 9px;
&__title {
width: 100%;
text-align: center;
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0 0 0 15px;
}
&__message {
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 10px 0 0 0;
text-align: center;
}
&__check-area {
margin-top: 27px;
font-size: 14px;
line-height: 19px;
color: #1b2533;
display: flex;
justify-content: center;
align-items: center;
&__checkbox {
margin: 0 10px 0 0;
}
}
}
&__choosing {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 25px;
&__label {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin: 0;
}
&__right {
display: flex;
align-items: center;
&__option {
font-size: 14px;
line-height: 17px;
color: #768394;
margin: 0;
cursor: pointer;
border-bottom: 3px solid #fff;
}
}
}
&__value-area {
width: 100%;
display: flex;
align-items: flex-start;
&__mnemonic {
display: flex;
background: #f5f6fa;
border-radius: 9px;
padding: 10px;
width: calc(100% - 20px);
&__value {
font-family: 'Source Code Pro', sans-serif;
font-size: 14px;
line-height: 25px;
color: #384b65;
word-break: break-word;
margin: 0;
word-spacing: 8px;
}
&__button {
margin-left: 10px;
min-width: 66px;
min-height: 30px;
}
}
&__password {
width: 100%;
margin: 10px 0 20px 0;
&__input {
width: calc(100% - 2px);
}
}
}
}
}
.left-option {
margin-right: 15px;
}
.active {
font-family: 'font_medium', sans-serif;
color: #0068dc;
border-bottom: 3px solid #0068dc;
}
.error {
color: red;
}
::v-deep .label-container {
&__main {
margin-bottom: 10px;
&__label {
margin: 0;
font-size: 14px;
line-height: 19px;
color: #7c8794;
font-family: 'font_bold', sans-serif;
}
&__error {
margin: 0 0 0 10px;
font-size: 14px;
line-height: 19px;
}
}
}
</style>

View File

@ -57,6 +57,7 @@ import WarningIcon from '@/../static/images/accessGrants/warning.svg';
import { RouteConfig } from '@/router';
import { MetaUtils } from '@/utils/meta';
import { Download } from "@/utils/download";
// @vue/component
@Component({
@ -106,22 +107,10 @@ export default class ResultStep extends Vue {
* Downloads a file with the access called access-grant-<timestamp>.key
*/
public onDownloadGrantClick(): void {
// this code is based on this Stackoverflow response: https://stackoverflow.com/a/33542499
// It works for downloading a file in IE 10+, Firefox, and Chrome without any additional libraries
const blob = new Blob([this.access], {type: 'text/plain'});
const ts = new Date();
const filename = 'access-grant-' + ts.toJSON() + '.key';
if (window.navigator.msSaveBlob) {
window.navigator.msSaveBlob(blob, filename);
} else {
const elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = filename;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
Download.file(this.access, filename);
this.$notify.success('Token was downloaded successfully');
}

View File

@ -2,173 +2,145 @@
// See LICENSE for copying information.
<template>
<div class="generate-container">
<h1 class="generate-container__title">Encryption Passphrase</h1>
<div class="generate-container__choosing">
<p class="generate-container__choosing__label">Passphrase</p>
<div class="generate-container__choosing__right">
<p
class="generate-container__choosing__right__option left-option"
:class="{ active: isGenerateState }"
@click="onChooseGenerate"
>
Generate Phrase
</p>
<p
class="generate-container__choosing__right__option"
:class="{ active: isEnterState }"
@click="onChooseCreate"
>
Enter Phrase
</p>
<div class="encrypt-container">
<EncryptIcon />
<h1 class="encrypt-container__title">Encrypt your data</h1>
<p class="encrypt-container__info">
The encryption passphrase is used to encrypt and access the data that you upload to Storj DCS. We strongly
encourage you to use a mnemonic phrase, which is automatically generated one on the client-side for you.
<span v-if="isOnboardingTour">
Alternatively, you can skip this step and enter a passphrase later into the Uplink CLI during setup.
</span>
</p>
<div class="encrypt-container__header">
<p class="encrypt-container__header__rec">RECOMMENDED</p>
<div class="encrypt-container__header__row">
<p class="encrypt-container__header__row__gen" :class="{ active: isGenerate }" @click="setToGenerate">Generate Phrase</p>
<div class="encrypt-container__header__row__right">
<p class="encrypt-container__header__row__right__enter" :class="{ active: !isGenerate }" @click="setToEnter">Enter Your Own Passphrase</p>
<VInfo
class="encrypt-container__header__row__right__info-button"
text="We strongly encourage you to use a mnemonic phrase, which is automatically generated one on the client-side for you. Alternatively, you can enter your own passphrase."
>
<InfoIcon class="encrypt-container__header__row__right__info-button__image" />
</VInfo>
</div>
</div>
</div>
<div v-if="isEnterState" class="generate-container__enter-passphrase-box">
<div class="generate-container__enter-passphrase-box__header">
<GreenWarningIcon />
<h2 class="generate-container__enter-passphrase-box__header__label">Enter an Existing Passphrase</h2>
</div>
<p class="generate-container__enter-passphrase-box__message">
if you already have an encryption passphrase, enter your encryption passphrase here.
<div v-if="isGenerate" class="encrypt-container__generate">
<p class="encrypt-container__generate__value">{{ passphrase }}</p>
<VButton
class="encrypt-container__generate__button"
label="Copy"
width="66px"
height="30px"
is-blue-white="true"
:on-press="onCopyClick"
/>
</div>
<div v-else class="encrypt-container__enter">
<HeaderlessInput
placeholder="Enter a passphrase here..."
width="100%"
:error="enterError"
@setData="setPassphrase"
/>
</div>
<div class="encrypt-container__save">
<h2 class="encrypt-container__save__title">Save your encryption passphrase</h2>
<p class="encrypt-container__save__msg">
Please note that Storj does not know or store your encryption passphrase. If you lose it, you will
not be able to recover your files.
</p>
<p class="encrypt-container__save__download" @click="onDownloadClick">Download as a text file</p>
</div>
<div class="generate-container__value-area">
<div v-if="isGenerateState" class="generate-container__value-area__mnemonic">
<p class="generate-container__value-area__mnemonic__value">{{ passphrase }}</p>
<VButton
class="generate-container__value-area__mnemonic__button"
label="Copy"
width="66px"
height="30px"
:on-press="onCopyClick"
/>
</div>
<div v-else class="generate-container__value-area__password">
<HeaderedInput
class="generate-container__value-area__password__input"
placeholder="Enter encryption passphrase here"
:error="errorMessage"
@setData="onChangePassphrase"
/>
</div>
<div class="encrypt-container__buttons">
<VButton
v-if="isOnboardingTour"
class="encrypt-container__buttons__back"
label="< Back"
height="64px"
border-radius="62px"
:on-press="onBackClick"
is-grey-blue="true"
:is-disabled="isLoading"
/>
<VButton
v-if="isOnboardingTour"
class="encrypt-container__buttons__skip"
label="Skip for now"
height="64px"
border-radius="62px"
:on-press="onSkipClick"
is-grey-blue="true"
:is-disabled="isLoading"
/>
<VButton
label="Next >"
height="64px"
border-radius="62px"
:on-press="onNextButtonClick"
:is-disabled="isLoading"
/>
</div>
<div v-if="isGenerateState" class="generate-container__warning">
<h2 class="generate-container__warning__title">Save Your Encryption Passphrase</h2>
<p class="generate-container__warning__message">
Youll need this passphrase to access data in the future. This is the only time it will be displayed.
Be sure to write it down.
</p>
<label class="generate-container__warning__check-area" :class="{ error: isError }" for="pass-checkbox">
<input
id="pass-checkbox"
v-model="isChecked"
class="generate-container__warning__check-area__checkbox"
type="checkbox"
@change="isError = false"
>
Yes, I wrote this down or saved it somewhere.
</label>
</div>
<VButton
class="generate-container__next-button"
label="Next"
width="100%"
height="48px"
:on-press="onProceed"
:is-disabled="isButtonDisabled"
/>
</div>
</template>
<script lang="ts">
import * as bip39 from 'bip39';
import { Component, Prop, Vue } from 'vue-property-decorator';
import * as bip39 from "bip39";
import { RouteConfig } from "@/router";
import { LocalData, UserIDPassSalt } from "@/utils/localData";
import { Download } from "@/utils/download";
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import VButton from '@/components/common/VButton.vue';
import VInfo from "@/components/common/VInfo.vue";
import HeaderlessInput from "@/components/common/HeaderlessInput.vue";
import GreenWarningIcon from '@/../static/images/accessGrants/greenWarning.svg';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import EncryptIcon from "@/../static/images/objects/encrypt.svg";
import InfoIcon from "@/../static/images/common/greyInfo.svg";
// @vue/component
@Component({
components: {
GreenWarningIcon,
EncryptIcon,
InfoIcon,
VInfo,
VButton,
HeaderedInput,
HeaderlessInput,
},
})
export default class GeneratePassphrase extends Vue {
@Prop({ default: () => null })
public readonly onButtonClick: () => void;
public readonly onNextClick: () => unknown;
@Prop({ default: () => null })
public readonly onBackClick: () => unknown;
@Prop({ default: () => null })
public readonly onSkipClick: () => unknown;
@Prop({ default: () => null })
public readonly setParentPassphrase: (passphrase: string) => void;
@Prop({ default: false })
public readonly isLoading: boolean;
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
public isGenerateState = true;
public isEnterState = false;
public isChecked = false;
public isError = false;
public isGenerate = true;
public enterError = '';
public passphrase = '';
public errorMessage = '';
/**
* Lifecycle hook after initial render.
* Generates mnemonic string.
* Chooses correct state and generates mnemonic.
*/
public mounted(): void {
this.passphrase = bip39.generateMnemonic();
this.setParentPassphrase(this.passphrase);
}
public onProceed(): void {
if (!this.passphrase) {
this.errorMessage = 'Passphrase can\'t be empty';
const idPassSalt: UserIDPassSalt | null = LocalData.getUserIDPassSalt();
if (idPassSalt && idPassSalt.userId === this.$store.getters.user.id) {
this.isGenerate = false;
return;
}
if (!this.isChecked && this.isGenerateState) {
this.isError = true;
return;
}
this.analytics.eventTriggered(AnalyticsEvent.PASSPHRASE_CREATED);
this.onButtonClick();
}
/**
* Changes state to generate passphrase.
*/
public onChooseGenerate(): void {
if (this.passphrase && this.isGenerateState) return;
this.passphrase = bip39.generateMnemonic();
this.setParentPassphrase(this.passphrase);
this.isEnterState = false;
this.isGenerateState = true;
}
/**
* Changes state to create passphrase.
*/
public onChooseCreate(): void {
if (this.passphrase && this.isEnterState) return;
this.errorMessage = '';
this.passphrase = '';
this.setParentPassphrase(this.passphrase);
this.isEnterState = true;
this.isGenerateState = false;
}
/**
@ -181,219 +153,225 @@ export default class GeneratePassphrase extends Vue {
}
/**
* Changes passphrase data from input value.
* @param value
* Holds on download button click logic.
* Downloads encryption passphrase as a txt file.
*/
public onChangePassphrase(value: string): void {
this.passphrase = value.trim();
this.setParentPassphrase(this.passphrase);
this.errorMessage = '';
public onDownloadClick(): void {
if (!this.passphrase) {
this.enterError = 'Can\'t be empty!';
return;
}
const fileName = 'StorjEncryptionPassphrase.txt';
Download.file(this.passphrase, fileName);
}
/**
* Indicates if button is disabled.
* Sets passphrase from child component.
*/
public get isButtonDisabled(): boolean {
return this.isLoading || !this.passphrase || (!this.isChecked && this.isGenerateState);
public setPassphrase(passphrase: string): void {
if (this.enterError) this.enterError = '';
this.passphrase = passphrase;
this.setParentPassphrase(this.passphrase);
}
/**
* Sets view state to enter passphrase.
*/
public setToEnter(): void {
this.passphrase = '';
this.isGenerate = false;
}
/**
* Sets view state to generate passphrase.
*/
public setToGenerate(): void {
if (this.enterError) this.enterError = '';
this.passphrase = bip39.generateMnemonic();
this.isGenerate = true;
}
/**
* Holds on next button click logic.
*/
public async onNextButtonClick(): Promise<void> {
if (!this.passphrase) {
this.enterError = 'Can\'t be empty!';
return;
}
await this.onNextClick();
}
/**
* Indicates if current route is onboarding tour.
*/
public get isOnboardingTour(): boolean {
return this.$route.path.includes(RouteConfig.OnboardingTour.path);
}
}
</script>
<style scoped lang="scss">
.generate-container {
padding: 25px 50px;
max-width: 515px;
min-width: 515px;
.encrypt-container {
font-family: 'font_regular', sans-serif;
font-style: normal;
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
border-radius: 6px;
padding: 60px;
max-width: 500px;
background: #fcfcfc;
box-shadow: 0 0 32px rgba(0, 0, 0, 0.04);
border-radius: 20px;
margin: 30px auto 0 auto;
&__title {
font-family: 'font_bold', sans-serif;
font-weight: bold;
font-size: 22px;
line-height: 27px;
color: #000;
margin: 0 0 30px 0;
font-size: 36px;
line-height: 56px;
letter-spacing: 1px;
color: #14142b;
margin: 35px 0 10px 0;
}
&__enter-passphrase-box {
padding: 20px;
background: #f9fffc;
border: 1px solid #1a9666;
border-radius: 9px;
&__info {
font-size: 16px;
line-height: 32px;
letter-spacing: 0.75px;
color: #1b2533;
margin-bottom: 20px;
}
&__header {
&__header {
&__rec {
font-size: 12px;
line-height: 15px;
color: #1b2533;
opacity: 0.4;
margin-bottom: 15px;
}
&__row {
display: flex;
align-items: center;
margin-bottom: 10px;
justify-content: space-between;
&__label {
&__gen {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0 0 0 10px;
color: #a9b5c1;
padding-bottom: 10px;
border-bottom: 5px solid #fff;
cursor: pointer;
}
}
&__message {
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0;
}
}
&__right {
display: flex;
align-items: flex-start;
&__warning {
display: flex;
flex-direction: column;
padding: 20px;
width: calc(100% - 40px);
margin: 35px 0;
background: #fff;
border: 1px solid #e6e9ef;
border-radius: 9px;
&__enter {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #a9b5c1;
cursor: pointer;
margin-right: 10px;
padding-bottom: 10px;
border-bottom: 5px solid #fff;
}
&__title {
width: 100%;
text-align: center;
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 0 0 0 15px;
}
&__info-button {
&__message {
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin: 10px 0 0 0;
text-align: center;
}
&__check-area {
margin-top: 27px;
font-size: 14px;
line-height: 19px;
color: #1b2533;
display: flex;
justify-content: center;
align-items: center;
&__checkbox {
margin: 0 10px 0 0;
&__image {
cursor: pointer;
}
}
}
}
}
&__choosing {
&__generate {
margin-top: 25px;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 25px;
padding: 25px;
background: #eff0f7;
border-radius: 10px;
&__label {
&__value {
font-size: 16px;
line-height: 28px;
color: #384b65;
}
&__button {
margin-left: 32px;
min-width: 66px;
}
}
&__enter {
margin-top: 25px;
}
&__save {
border: 1px solid #e6e9ef;
border-radius: 10px;
padding: 25px;
margin-top: 35px;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin: 0;
line-height: 19px;
color: #1b2533;
margin-bottom: 10px;
}
&__right {
display: flex;
align-items: center;
&__msg {
font-size: 14px;
line-height: 20px;
color: #1b2533;
margin-bottom: 10px;
}
&__option {
font-size: 14px;
line-height: 17px;
color: #768394;
margin: 0;
cursor: pointer;
border-bottom: 3px solid #fff;
}
&__download {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #0068dc;
cursor: pointer;
}
}
&__value-area {
&__buttons {
width: 100%;
display: flex;
align-items: flex-start;
align-items: center;
margin-top: 30px;
&__mnemonic {
display: flex;
background: #f5f6fa;
border-radius: 9px;
padding: 10px;
width: calc(100% - 20px);
&__value {
font-family: 'Source Code Pro', sans-serif;
font-size: 14px;
line-height: 25px;
color: #384b65;
word-break: break-word;
margin: 0;
word-spacing: 8px;
}
&__button {
margin-left: 10px;
min-width: 66px;
min-height: 30px;
}
}
&__password {
width: 100%;
margin: 10px 0 20px 0;
&__input {
width: calc(100% - 2px);
}
&__back,
&__skip {
margin-right: 24px;
}
}
}
.left-option {
margin-right: 15px;
}
.active {
font-family: 'font_medium', sans-serif;
color: #0068dc;
border-bottom: 3px solid #0068dc;
color: #0149ff;
border-color: #0149ff;
}
.error {
color: red;
}
::v-deep .info__box__message {
min-width: 440px;
::v-deep .label-container {
&__main {
margin-bottom: 10px;
&__label {
margin: 0;
font-size: 14px;
line-height: 19px;
color: #7c8794;
font-family: 'font_bold', sans-serif;
}
&__error {
margin: 0 0 0 10px;
font-size: 14px;
line-height: 19px;
}
&__regular-text {
line-height: 32px;
}
}
</style>

View File

@ -36,7 +36,7 @@ export default class VButton extends Vue {
@Prop({default: false})
private readonly isDeletion: boolean;
@Prop({default: false})
private readonly isBack: boolean;
private readonly isGreyBlue: boolean;
@Prop({default: false})
private readonly isBlueWhite: boolean;
@Prop({default: false})
@ -57,7 +57,7 @@ export default class VButton extends Vue {
if (this.isDeletion) return 'container red';
if (this.isBack) return 'container back';
if (this.isGreyBlue) return 'container grey-blue';
if (this.isBlueWhite) return 'container blue-white';
@ -94,7 +94,7 @@ export default class VButton extends Vue {
}
}
.back {
.grey-blue {
background-color: #fff !important;
border: 2px solid #d9dbe9 !important;
@ -153,7 +153,7 @@ export default class VButton extends Vue {
}
}
&.back {
&.grey-blue {
background-color: #2683ff !important;
border-color: #2683ff !important;

View File

@ -109,7 +109,7 @@ export default class HeaderArea extends Vue {
* Redirects to onboarding tour.
*/
public async onStartTourButtonClick(): Promise<void> {
await this.$router.push(RouteConfig.OnboardingTour.path)
await this.$router.push(RouteConfig.OnboardingTour.path).catch(() => {return; })
}
/**

View File

@ -4,163 +4,47 @@
<template>
<div class="encrypt">
<h1 class="encrypt__title">Objects</h1>
<div class="encrypt__container">
<EncryptIcon />
<h1 class="encrypt__container__title">Encrypt your data</h1>
<p class="encrypt__container__info">
The encryption passphrase is used to encrypt and access the data that you upload to Storj DCS.
</p>
<div class="encrypt__container__header">
<p class="encrypt__container__header__rec">RECOMMENDED</p>
<div class="encrypt__container__header__row">
<p class="encrypt__container__header__row__gen" :class="{ active: isGenerate }" @click="setToGenerate">Generate Phrase</p>
<div class="encrypt__container__header__row__right">
<p class="encrypt__container__header__row__right__enter" :class="{ active: !isGenerate }" @click="setToEnter">Enter Your Own Passphrase</p>
<VInfo
class="encrypt__container__header__row__right__info-button"
text="We strongly encourage you to use a mnemonic phrase, which is automatically generated one on the client-side for you. Alternatively, you can enter your own passphrase."
>
<InfoIcon class="encrypt__container__header__row__right__info-button__image" />
</VInfo>
</div>
</div>
</div>
<div v-if="isGenerate" class="encrypt__container__generate">
<p class="encrypt__container__generate__value">{{ passphrase }}</p>
<VButton
class="encrypt__container__generate__button"
label="Copy"
width="66px"
height="30px"
is-blue-white="true"
:on-press="onCopyClick"
/>
</div>
<div v-else class="encrypt__container__enter">
<HeaderlessInput
placeholder="Enter a passphrase here..."
width="100%"
:error="enterError"
@setData="setPassphrase"
/>
</div>
<div class="encrypt__container__save">
<h2 class="encrypt__container__save__title">Save your encryption passphrase</h2>
<p class="encrypt__container__save__msg">
Please note that Storj does not know or store your encryption passphrase. If you lose it, you will
not be able to recover your files.
</p>
</div>
<div class="encrypt__container__buttons">
<VButton
label="Next >"
width="100%"
height="64px"
border-radius="62px"
:on-press="onNextClick"
/>
</div>
</div>
<GeneratePassphrase
:on-next-click="onNextClick"
:set-parent-passphrase="setPassphrase"
:is-loading="isLoading"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import pbkdf2 from 'pbkdf2';
import * as bip39 from 'bip39';
import { RouteConfig } from '@/router';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { LocalData, UserIDPassSalt } from '@/utils/localData';
import { LocalData } from "@/utils/localData";
import VInfo from '@/components/common/VInfo.vue';
import VButton from '@/components/common/VButton.vue';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import EncryptIcon from '@/../static/images/objects/encrypt.svg';
import InfoIcon from '@/../static/images/common/greyInfo.svg';
import GeneratePassphrase from "@/components/common/GeneratePassphrase.vue";
// @vue/component
@Component({
components: {
EncryptIcon,
InfoIcon,
VInfo,
VButton,
HeaderlessInput,
GeneratePassphrase,
},
})
export default class EncryptData extends Vue {
private isLoading = false;
private keyToBeStored = '';
public isGenerate = true;
public enterError = '';
public isLoading = false;
public passphrase = '';
/**
* Lifecycle hook after initial render.
* Chooses correct state and generates mnemonic.
*/
public mounted(): void {
const idPassSalt: UserIDPassSalt | null = LocalData.getUserIDPassSalt();
if (idPassSalt && idPassSalt.userId === this.$store.getters.user.id) {
this.isGenerate = false;
return;
}
this.passphrase = bip39.generateMnemonic();
}
/**
* Holds on copy button click logic.
* Copies passphrase to clipboard.
*/
public onCopyClick(): void {
this.$copyText(this.passphrase);
this.$notify.success('Passphrase was copied successfully');
}
/**
* Sets passphrase from child component.
*/
public setPassphrase(passphrase: string): void {
if (this.enterError) this.enterError = '';
this.passphrase = passphrase;
}
/**
* Sets view state to enter passphrase.
*/
public setToEnter(): void {
this.passphrase = '';
this.isGenerate = false;
}
/**
* Sets view state to generate passphrase.
*/
public setToGenerate(): void {
if (this.enterError) this.enterError = '';
this.passphrase = bip39.generateMnemonic();
this.isGenerate = true;
}
/**
* Holds on next button click logic.
*/
public async onNextClick(): Promise<void> {
if (this.isLoading) return;
if (!this.passphrase) {
this.enterError = 'Can\'t be empty';
return;
}
this.isLoading = true;
const SALT = 'storj-unique-salt';
@ -173,9 +57,9 @@ export default class EncryptData extends Vue {
return;
}
this.keyToBeStored = await result.toString('hex');
const keyToBeStored = await result.toString('hex');
await LocalData.setUserIDPassSalt(this.$store.getters.user.id, this.keyToBeStored, SALT);
await LocalData.setUserIDPassSalt(this.$store.getters.user.id, keyToBeStored, SALT);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
this.isLoading = false;
@ -211,149 +95,5 @@ export default class EncryptData extends Vue {
line-height: 26px;
color: #232b34;
}
&__container {
padding: 60px;
max-width: 500px;
background: #fcfcfc;
box-shadow: 0 0 32px rgba(0, 0, 0, 0.04);
border-radius: 20px;
margin: 30px auto 0 auto;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 36px;
line-height: 56px;
letter-spacing: 1px;
color: #14142b;
margin: 35px 0 10px 0;
}
&__info {
font-size: 16px;
line-height: 32px;
letter-spacing: 0.75px;
color: #1b2533;
margin-bottom: 20px;
}
&__header {
&__rec {
font-size: 12px;
line-height: 15px;
color: #1b2533;
opacity: 0.4;
margin-bottom: 15px;
}
&__row {
display: flex;
align-items: center;
justify-content: space-between;
&__gen {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #a9b5c1;
padding-bottom: 10px;
border-bottom: 5px solid #fff;
cursor: pointer;
}
&__right {
display: flex;
align-items: flex-start;
&__enter {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #a9b5c1;
cursor: pointer;
margin-right: 10px;
padding-bottom: 10px;
border-bottom: 5px solid #fff;
}
&__info-button {
&__image {
cursor: pointer;
}
}
}
}
}
&__generate {
margin-top: 25px;
display: flex;
align-items: center;
padding: 25px;
background: #eff0f7;
border-radius: 10px;
&__value {
font-size: 16px;
line-height: 28px;
color: #384b65;
}
&__button {
margin-left: 32px;
min-width: 66px;
}
}
&__enter {
margin-top: 25px;
}
&__save {
border: 1px solid #e6e9ef;
border-radius: 10px;
padding: 25px;
margin-top: 35px;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 19px;
color: #1b2533;
margin-bottom: 12px;
}
&__msg {
font-size: 14px;
line-height: 20px;
color: #1b2533;
}
}
&__buttons {
display: flex;
align-items: center;
margin-top: 30px;
&__back {
margin-right: 24px;
}
}
}
}
.active {
color: #0149ff;
border-color: #0149ff;
}
::v-deep .info__box__message {
min-width: 440px;
&__regular-text {
line-height: 32px;
}
}
</style>

View File

@ -21,6 +21,6 @@ export default class OnboardingTourArea extends Vue {}
display: flex;
flex-direction: column;
align-items: center;
padding: 110px 0 80px 0;
padding: 45px 0 60px 0;
}
</style>

View File

@ -32,12 +32,12 @@
:is-disabled="isLoading"
/>
</div>
<div
<router-link
class="overview-area__skip-button"
@click.prevent="onSkipClick"
:to="projectDashboardPath"
>
Skip and go directly to dashboard
</div>
</router-link>
</div>
</template>
@ -62,6 +62,7 @@ import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
})
export default class OverviewStep extends Vue {
public isLoading = false;
public projectDashboardPath = RouteConfig.ProjectDashboard.path;
private readonly analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
@ -93,14 +94,6 @@ export default class OverviewStep extends Vue {
this.isLoading = false;
}
/**
* Holds button click logic.
* Redirects to project dashboard.
*/
public async onSkipClick(): Promise<void> {
await this.$router.push(RouteConfig.ProjectDashboard.path);
}
}
</script>
@ -149,7 +142,7 @@ export default class OverviewStep extends Vue {
}
&__skip-button {
margin: 50px 0 40px 0;
margin-top: 50px;
color: #b7c1ca;
cursor: pointer;
text-decoration: underline !important;

View File

@ -2,13 +2,155 @@
// See LICENSE for copying information.
<template>
<div class="encrypt" />
<div class="encrypt">
<GeneratePassphrase
:on-next-click="onNextClick"
:on-skip-click="onSkipClick"
:on-back-click="onBackClick"
:set-parent-passphrase="setPassphrase"
:is-loading="isLoading"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { RouteConfig } from "@/router";
import { MetaUtils } from "@/utils/meta";
import { ACCESS_GRANTS_ACTIONS, ACCESS_GRANTS_MUTATIONS } from "@/store/modules/accessGrants";
import { AccessGrant } from "@/types/accessGrants";
import GeneratePassphrase from "@/components/common/GeneratePassphrase.vue";
// @vue/component
@Component
export default class EncryptYourData extends Vue {}
@Component({
components: {
GeneratePassphrase,
}
})
export default class EncryptYourData extends Vue {
private worker: Worker;
public isLoading = true;
public passphrase = '';
/**
* Lifecycle hook after initial render.
* Sets local web worker.
*/
public mounted(): void {
this.setWorker();
this.isLoading = false;
}
/**
* Sets passphrase from child component.
*/
public setPassphrase(passphrase: string): void {
this.passphrase = passphrase;
}
/**
* Sets local worker with worker instantiated in store.
* Also sets worker's onerror logic.
*/
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.
* Generates access grant.
*/
public async onNextClick(): Promise<void> {
if (this.isLoading) return;
this.isLoading = true;
try {
const restrictedKey = await this.generateRestrictedKey();
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': restrictedKey,
'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) {
await this.$notify.error(accessGrantEvent.data.error);
}
await this.$store.commit(ACCESS_GRANTS_MUTATIONS.SET_ONBOARDING_ACCESS_GRANT, accessGrantEvent.data.value);
await this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.CLIStep.with(RouteConfig.GeneratedAG)).path);
} catch (error) {
await this.$notify.error(error.message)
}
this.isLoading = false;
}
/**
* Holds on skip button click logic.
* Generates CLI API key.
*/
public async onSkipClick(): Promise<void> {
if (this.isLoading) return;
this.isLoading = true;
try {
const restrictedKey = await this.generateRestrictedKey();
await this.$store.commit(ACCESS_GRANTS_MUTATIONS.SET_ONBOARDING_CLI_API_KEY, restrictedKey);
await this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.CLIStep.with(RouteConfig.APIKey)).path);
} catch (error) {
await this.$notify.error(error.message)
}
this.isLoading = false;
}
/**
* Holds on back button click logic.
* Navigates to previous screen.
*/
public async onBackClick(): Promise<void> {
await this.$router.push(RouteConfig.OnboardingTour.path).catch(() => {return; })
}
/**
* Generates and returns restricted key from clean API key.
*/
private async generateRestrictedKey(): Promise<string> {
const date = new Date().toISOString()
const onbAGName = `Onboarding Grant ${date}`
const cleanAPIKey: AccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, onbAGName);
await this.worker.postMessage({
'type': 'SetPermission',
'isDownload': true,
'isUpload': true,
'isList': true,
'isDelete': true,
'buckets': [],
'apiKey': cleanAPIKey.secret,
});
const grantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
if (grantEvent.data.error) {
throw new Error(grantEvent.data.error)
}
return grantEvent.data.value;
}
}
</script>

View File

@ -45,6 +45,8 @@ export const ACCESS_GRANTS_MUTATIONS = {
SET_SEARCH_QUERY: 'setAccessGrantsSearchQuery',
SET_PAGE_NUMBER: 'setAccessGrantsPage',
SET_DURATION_PERMISSION: 'setAccessGrantsDurationPermission',
SET_ONBOARDING_CLI_API_KEY: 'setOnboardingCLIApiKey',
SET_ONBOARDING_ACCESS_GRANT: 'setOnboardingAccessGrant',
};
const {
@ -61,6 +63,8 @@ const {
SET_GATEWAY_CREDENTIALS,
SET_ACCESS_GRANTS_WEB_WORKER,
STOP_ACCESS_GRANTS_WEB_WORKER,
SET_ONBOARDING_CLI_API_KEY,
SET_ONBOARDING_ACCESS_GRANT,
} = ACCESS_GRANTS_MUTATIONS;
export class AccessGrantsState {
@ -73,6 +77,8 @@ export class AccessGrantsState {
public gatewayCredentials: GatewayCredentials = new GatewayCredentials();
public accessGrantsWebWorker: Worker | null = null;
public isAccessGrantsWebWorkerReady = false;
public onboardingCLIApiKey: string;
public onboardingAccessGrant: string;
}
interface AccessGrantsContext {
@ -140,6 +146,12 @@ export function makeAccessGrantsModule(api: AccessGrantsApi): StoreModule<Access
state.permissionNotBefore = permission.notBefore;
state.permissionNotAfter = permission.notAfter;
},
[SET_ONBOARDING_CLI_API_KEY](state: AccessGrantsState, apiKey: string) {
state.onboardingCLIApiKey = apiKey;
},
[SET_ONBOARDING_ACCESS_GRANT](state: AccessGrantsState, accessGrant: string) {
state.onboardingAccessGrant = accessGrant;
},
[CHANGE_SORT_ORDER](state: AccessGrantsState, order: AccessGrantsOrderBy) {
state.cursor.order = order;
},

View File

@ -0,0 +1,22 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* DownloadTXT is used to download some content as a file.
*/
export class Download {
public static file(content: string, name: string): void {
const blob = new Blob([content], {type: 'text/plain'});
if (window.navigator.msSaveBlob) {
window.navigator.msSaveBlob(blob, name);
} else {
const elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = name;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
}

View File

@ -70,9 +70,8 @@ exports[`OverviewStep.vue renders correctly 1`] = `
<p class="overview-container__encryption-container">The Uplink CLI uses end-to-end encryption for object data, metadata and path data.</p>
<div class="container" style="width: 100%; height: 64px; border-radius: 62px;"><span class="label">CONTINUE IN CLI</span></div>
</div>
</div>
<div class="overview-area__skip-button">
</div> <a href="/project-dashboard" class="overview-area__skip-button">
Skip and go directly to dashboard
</div>
</a>
</div>
`;