web/satellite: import file browser component

WHAT:
import and instantiate file browser component

WHY:
to operate over folders and objects

Change-Id: Ib6fb4fdc2668d2f274df3d1b23f8cc0bb6a361ea
This commit is contained in:
Vitalii Shpital 2021-03-23 22:50:34 +02:00
parent 444b1f4757
commit 6ae2351389
21 changed files with 1893 additions and 2897 deletions

View File

@ -88,6 +88,7 @@ type Config struct {
BetaSatelliteSupportURL string `help:"url link for for beta satellite support" default:""`
DocumentationURL string `help:"url link to documentation" devDefault:"https://documentation.storj.io/" releaseDefault:"https://documentation.tardigrade.io/"`
CouponCodeUIEnabled bool `help:"indicates if user is allowed to add coupon codes to account" default:"false"`
FileBrowserFlowDisabled bool `help:"indicates if file browser flow is disabled" default:"true"`
RateLimit web.IPRateLimiterConfig
@ -311,6 +312,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
BetaSatelliteSupportURL string
DocumentationURL string
CouponCodeUIEnabled bool
FileBrowserFlowDisabled bool
}
data.ExternalAddress = server.config.ExternalAddress
@ -330,6 +332,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
data.BetaSatelliteSupportURL = server.config.BetaSatelliteSupportURL
data.DocumentationURL = server.config.DocumentationURL
data.CouponCodeUIEnabled = server.config.CouponCodeUIEnabled
data.FileBrowserFlowDisabled = server.config.FileBrowserFlowDisabled
if server.templates.index == nil {
server.log.Error("index template is not set")

View File

@ -103,6 +103,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# external endpoint of the satellite if hosted
# console.external-address: ""
# indicates if file browser flow is disabled
# console.file-browser-flow-disabled: true
# allow domains to embed the satellite in a frame, space separated
# console.frame-ancestors: tardigrade.io

View File

@ -20,6 +20,7 @@
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
<meta name="documentation-url" content="{{ .DocumentationURL }}">
<meta name="coupon-code-ui-enabled" content="{{ .CouponCodeUIEnabled }}">
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
<title>{{ .SatelliteName }}</title>
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAL8SURBVHgBxVcxTFNRFL01JvylrRsmNWETWWSybg5I3NTQBCdRFyfrDuoKMgMyOQgdTcDAJmrUrWWCgQIbhJKy0TJQpsc73P/S9/tfP+8/GnqSn1fo77/3nnPufe8nhAR1ETeoy7hJjpj93/ycu0+UuUVuEA5Y2xaifirEVpX/nvknnBFbgpMGUfmIKOkRLRT578oxXy6IJcFCialH0EyaaPoZBy7tEQ3NEY1IKd4/iidHwqYLijLA559cuY6dT0RjBU5AAYm9fiivLFnBKMGBTyeqQ4BXhXDwdqjUiKZkskOzREsbzeeBNRMCEiDgr12uYl1WNbnW/oc2iUys8jrQyyxhHRkM3hdgAMFBHQyGG/GDqyDlsSeS/npQC99jlEBpOnyX2XCF8sGhZLbeMLMZkCDbJ1nYYTfDeMP9fMH5y5vmIKYE8RxUjBXPedDH1Zu6I9QFSzLQxErz4Xn5oNwg+2NSmuv3Lkvz4QlTi8rupDlBmA6tqQLrnYNCvoxSNAOtUEaakwzMv+ALidTP2OlKKiSK75Cs6hy9NYFkjzmG1SBCIuUq0Za8pgydge8R9E+e10qNrGE1ikH5435mo11bQgr4B9LEgVUC0Npm1o+vcuvBxB1NYFsaaeC2XUuW/Xs7msC9Xqa+MMa9jQr1KtXAQoKYHakeskbIhDrVasdTbbVY4s8ZYld/9PWuyeTSHksFBjBFcZ+aH/j/yZk5gcAcgImgIX6MNsKKhKBta1sB2A3HV5pD6iJQIzw/MICwoohc1F6ALBH03XemFYPl+VdzcBNUh6j5gZZEcP341opAAnX/AXl/A0FlrrshgMRR+YUvPPN8CHgAxlqWVYuEdH7V/ZilA6cosFDa53EcmUDKC+7X+IwxHEVhO0DK6aeXH88uHcWQA8xE7Yg69M6xgdWZUEFtNNDyx1s2KnyDIxu22zdZTjgWhANm/vL6clGIsnw3+Fbk94RreS8AMGrBxvwoT0lMPnSNC2JJoAPdgnMBJLjKq5lzAp1C19+OzwFiYzAU5f7eeQAAAABJRU5ErkJggg==" type="image/x-icon">
<link rel="dns-prefetch" href="https://js.stripe.com">

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,8 @@
"dev": "vue-cli-service build --mode development"
},
"dependencies": {
"@aws-sdk/client-s3": "3.8.1",
"aws-sdk": "2.853.0",
"browser": "git+https://github.com/storj/browser#7e16121e9070e671e3c68557048ced234c61943b",
"apollo-cache-inmemory": "1.6.6",
"apollo-client": "2.6.10",
"apollo-link": "1.2.14",

View File

@ -157,16 +157,17 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
*
* @param accessGrant - generated access grant
* @param optionalURL - optional requestURL
* @param isPublic - optional status
* @throws Error
*/
public async getGatewayCredentials(accessGrant: string, optionalURL?: string): Promise<GatewayCredentials> {
public async getGatewayCredentials(accessGrant: string, optionalURL?: string, isPublic?: boolean): Promise<GatewayCredentials> {
const requestURL: string = optionalURL || MetaUtils.getMetaContent('gateway-credentials-request-url');
if (!requestURL) throw new Error('Cannot get gateway credentials: request URL is not provided');
const path = `${requestURL}/v1/access`;
const body = {
access_grant: accessGrant,
public: false,
public: isPublic || false,
};
const response = await this.client.post(path, JSON.stringify(body));
if (!response.ok) {

View File

@ -173,6 +173,7 @@ export default class ConfirmDeletePopup extends Vue {
&__item {
padding: 25px;
width: calc(100% - 50px);
max-width: calc(100% - 50px);
background: rgba(245, 246, 250, 0.6);
&__name {
@ -182,6 +183,7 @@ export default class ConfirmDeletePopup extends Vue {
font-size: 14px;
line-height: 19px;
color: #1b2533;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -22,6 +22,7 @@ import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { USER_ACTIONS } from '@/store/modules/users';
import {
@ -63,6 +64,7 @@ export default class AccountDropdown extends Vue {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.STOP_ACCESS_GRANTS_WEB_WORKER);
await this.$store.dispatch(NOTIFICATION_ACTIONS.CLEAR);
await this.$store.dispatch(BUCKET_ACTIONS.CLEAR);
await this.$store.dispatch(OBJECTS_ACTIONS.CLEAR);
await this.$store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
LocalData.removeUserId();

View File

@ -31,6 +31,7 @@ import TeamIcon from '@/../static/images/navigation/team.svg';
import { RouteConfig } from '@/router';
import { NavigationLink } from '@/types/navigation';
import { MetaUtils } from '@/utils/meta';
@Component({
components: {
@ -42,16 +43,30 @@ import { NavigationLink } from '@/types/navigation';
},
})
export default class NavigationArea extends Vue {
public navigation: NavigationLink[] = [];
/**
* Array of navigation links with icons.
* Lifecycle hook before initial render.
* Sets navigation side bar list.
*/
public readonly navigation: NavigationLink[] = [
RouteConfig.ProjectDashboard.withIcon(DashboardIcon),
// TODO: enable when the flow will be finished
// RouteConfig.Objects.withIcon(ObjectsIcon),
RouteConfig.AccessGrants.withIcon(AccessGrantsIcon),
RouteConfig.Users.withIcon(TeamIcon),
];
public async beforeMount(): Promise<void> {
if (await JSON.parse(MetaUtils.getMetaContent('file-browser-flow-disabled'))) {
this.navigation = [
RouteConfig.ProjectDashboard.withIcon(DashboardIcon),
RouteConfig.AccessGrants.withIcon(AccessGrantsIcon),
RouteConfig.Users.withIcon(TeamIcon),
];
return;
}
this.navigation = [
RouteConfig.ProjectDashboard.withIcon(DashboardIcon),
RouteConfig.Objects.withIcon(ObjectsIcon),
RouteConfig.AccessGrants.withIcon(AccessGrantsIcon),
RouteConfig.Users.withIcon(TeamIcon),
];
}
/**
* Indicates if navigation side bar is hidden.

View File

@ -21,6 +21,7 @@
</template>
<script lang="ts">
import { Bucket } from 'aws-sdk/clients/s3';
import { Component, Prop, Vue } from 'vue-property-decorator';
import ObjectsPopup from '@/components/objects/ObjectsPopup.vue';
@ -29,8 +30,6 @@ import BucketIcon from '@/../static/images/objects/bucketItem.svg';
import DeleteIcon from '@/../static/images/objects/delete.svg';
import DotsIcon from '@/../static/images/objects/dots.svg';
import { Bucket } from '@aws-sdk/client-s3';
@Component({
components: {
BucketIcon,

View File

@ -11,14 +11,14 @@
</div>
</div>
<div class="buckets-view__loader" v-if="isLoading"/>
<p class="buckets-view__no-buckets" v-if="!(isLoading || buckets.length)">No Buckets</p>
<div class="buckets-view__list" v-if="!isLoading && buckets.length">
<p class="buckets-view__no-buckets" v-if="!(isLoading || bucketsList.length)">No Buckets</p>
<div class="buckets-view__list" v-if="!isLoading && bucketsList.length">
<div class="buckets-view__list__sorting-header">
<p class="buckets-view__list__sorting-header__name">Name</p>
<p class="buckets-view__list__sorting-header__date">Date Added</p>
<p class="buckets-view__list__sorting-header__empty"/>
</div>
<div class="buckets-view__list__item" v-for="(bucket, key) in buckets" :key="key" @click.stop="openBucket">
<div class="buckets-view__list__item" v-for="(bucket, key) in bucketsList" :key="key" @click.stop="openBucket(bucket.Name)">
<BucketItem
:item-data="bucket"
:show-delete-bucket-popup="showDeleteBucketPopup"
@ -55,6 +55,7 @@
</template>
<script lang="ts">
import { Bucket } from 'aws-sdk/clients/s3';
import { Component, Vue } from 'vue-property-decorator';
import BucketItem from '@/components/objects/BucketItem.vue';
@ -67,7 +68,6 @@ import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { AccessGrant, GatewayCredentials } from '@/types/accessGrants';
import { MetaUtils } from '@/utils/meta';
import { Bucket } from '@aws-sdk/client-s3';
@Component({
components: {
@ -102,85 +102,88 @@ export default class BucketsView extends Vue {
return;
}
await this.removeTemporaryAccessGrant();
try {
const cleanAPIKey: AccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, this.FILE_BROWSER_AG_NAME);
if (!this.accessGrantFromStore) {
await this.setWorker();
// This is done just in case old temporary access grant still exists.
await this.removeTemporaryAccessGrant();
await this.setAccess();
await this.fetchBuckets();
}
this.worker = this.$store.state.accessGrantsModule.accessGrantsWebWorker;
this.worker.onmessage = (event: MessageEvent) => {
const data = event.data;
if (data.error) {
throw new Error(data.error);
}
this.isLoading = false;
this.grantWithPermissions = data.value;
};
const now = new Date();
const inADay = new Date(now.setDate(now.getDate() + 1));
await this.worker.postMessage({
'type': 'SetPermission',
'isDownload': true,
'isUpload': true,
'isList': true,
'isDelete': true,
'buckets': [],
'apiKey': cleanAPIKey.secret,
'notBefore': now.toISOString(),
'notAfter': inADay.toISOString(),
});
// Timeout is used to give some time for web worker to return value.
setTimeout(() => {
this.worker.onmessage = (event: MessageEvent) => {
const data = event.data;
if (data.error) {
throw new Error(data.error);
}
this.accessGrant = data.value;
};
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': this.grantWithPermissions,
'passphrase': this.$route.params.passphrase,
'projectID': this.$store.getters.selectedProject.id,
'satelliteNodeURL': satelliteNodeURL,
});
// Timeout is used to give some time for web worker to return value.
setTimeout(async () => {
await this.$store.dispatch(OBJECTS_ACTIONS.SET_ACCESS_GRANT, this.accessGrant);
// TODO: use this value until all the satellites will have this URL set.
const gatewayURL = 'https://auth.tardigradeshare.io';
const gatewayCredentials: GatewayCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, {accessGrant: this.accessGrant, optionalURL: gatewayURL});
await this.$store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT);
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
this.isLoading = false;
if (!this.buckets.length) this.showCreateBucketPopup();
}, 1000);
}, 1000);
if (!this.bucketsList.length) this.showCreateBucketPopup();
} catch (error) {
await this.$notify.error(`Failed to setup Objects view. ${error.message}`);
return;
await this.$notify.error(`Failed to setup Buckets view. ${error.message}`);
}
}
/**
* Lifecycle hook before component destroying.
* Remove temporary created access grant.
* Sets access to S3 client.
*/
public async beforeDestroy(): Promise<void> {
await this.removeTemporaryAccessGrant();
public async setAccess(): Promise<void> {
const cleanAPIKey: AccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, this.FILE_BROWSER_AG_NAME);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_API_KEY, cleanAPIKey.secret);
const now = new Date();
const inADay = new Date(now.setDate(now.getDate() + 1));
await this.worker.postMessage({
'type': 'SetPermission',
'isDownload': true,
'isUpload': true,
'isList': true,
'isDelete': true,
'buckets': [],
'apiKey': cleanAPIKey.secret,
'notBefore': new Date().toISOString(),
'notAfter': inADay.toISOString(),
});
const grantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
this.grantWithPermissions = grantEvent.data.value;
if (grantEvent.data.error) {
throw new Error(grantEvent.data.error);
}
const satelliteNodeURL: string = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': this.grantWithPermissions,
'passphrase': this.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 gatewayCredentials: GatewayCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, {accessGrant: this.accessGrant});
await this.$store.dispatch(OBJECTS_ACTIONS.SET_GATEWAY_CREDENTIALS, gatewayCredentials);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_S3_CLIENT);
}
/**
* Fetches bucket using S3 client.
*/
public async fetchBuckets(): Promise<void> {
await this.$store.dispatch(OBJECTS_ACTIONS.FETCH_BUCKETS);
}
/**
* 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);
};
}
/**
@ -235,6 +238,12 @@ export default class BucketsView extends Vue {
* Opens utils dropdown.
*/
public openDropdown(key: number): void {
if (this.activeDropdown === key) {
this.activeDropdown = -1;
return;
}
this.activeDropdown = key;
}
@ -294,15 +303,33 @@ export default class BucketsView extends Vue {
}
}
public openBucket(): void {
/**
* Holds on bucket click. Proceeds to file browser.
*/
public openBucket(bucketName: string): void {
this.$store.dispatch(OBJECTS_ACTIONS.SET_FILE_COMPONENT_BUCKET_NAME, bucketName);
this.$router.push(RouteConfig.Objects.with(RouteConfig.UploadFile).path);
}
/**
* Returns fetched buckets from store.
*/
public get buckets(): Bucket[] {
return this.$store.state.objectsModule.buckets;
public get bucketsList(): Bucket[] {
return this.$store.state.objectsModule.bucketsList;
}
/**
* Returns passphrase from store.
*/
private get passphrase(): string {
return this.$store.state.objectsModule.passphrase;
}
/**
* Returns access grant from store.
*/
private get accessGrantFromStore(): string {
return this.$store.state.objectsModule.accessGrant;
}
}
</script>

View File

@ -22,7 +22,7 @@ import GeneratePassphrase from '@/components/common/GeneratePassphrase.vue';
import { RouteConfig } from '@/router';
import { OBJECTS_ACTIONS } from '@/store/modules/objects';
import { LocalData } from '@/utils/localData';
import { LocalData, UserIDPassSalt } from '@/utils/localData';
@Component({
components: {
@ -31,9 +31,21 @@ import { LocalData } from '@/utils/localData';
})
export default class CreatePassphrase extends Vue {
private isLoading: boolean = false;
private keyToBeStored: string = '';
public passphrase: string = '';
/**
* Lifecycle hook after initial render.
* Chooses correct route.
*/
public mounted(): void {
const idPassSalt: UserIDPassSalt | null = LocalData.getUserIDPassSalt();
if (idPassSalt && idPassSalt.userId === this.$store.getters.user.id) {
this.$router.push({name: RouteConfig.EnterPassphrase.name});
}
}
/**
* Sets passphrase from child component.
*/
@ -44,22 +56,43 @@ export default class CreatePassphrase extends Vue {
/**
* Holds on next button click logic.
*/
public onNextClick(): void {
public async onNextClick(): Promise<void> {
if (this.isLoading) return;
this.isLoading = true;
const SALT = 'storj-unique-salt';
pbkdf2.pbkdf2(this.passphrase, SALT, 1, 64, (error, key) => {
if (error) return this.$notify.error(error.message);
LocalData.setUserIDPassSalt(this.$store.getters.user.id, key.toString('hex'), SALT);
});
const result: Buffer | Error = await this.pbkdf2Async(SALT);
if (result instanceof Error) {
await this.$notify.error(result.message);
return;
}
this.keyToBeStored = await result.toString('hex');
await LocalData.setUserIDPassSalt(this.$store.getters.user.id, this.keyToBeStored, SALT);
await this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
this.isLoading = false;
this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
this.$router.push({name: RouteConfig.BucketsManagement.name});
await this.$router.push({name: RouteConfig.EnterPassphrase.name});
}
/**
* Generates passphrase fingerprint asynchronously.
*/
private pbkdf2Async(salt: string): Promise<Buffer | Error> {
const ITERATIONS = 1;
const KEY_LENGTH = 64;
return new Promise((response, reject) => {
pbkdf2.pbkdf2(this.passphrase, salt, ITERATIONS, KEY_LENGTH, (error, key) => {
error ? reject(error) : response(key);
});
});
}
}
</script>

View File

@ -13,14 +13,6 @@
</div>
<p class="enter-pass__container__warning__message">
Entering your encryption passphrase here will share encryption data with your browser.
<a
class="enter-pass__container__warning__message__link"
:href="docsLink"
target="_blank"
rel="noopener norefferer"
>
Learn More
</a>
</p>
</div>
<label class="enter-pass__container__textarea" for="enter-pass-textarea">
@ -38,8 +30,9 @@
<div class="enter-pass__container__error" v-if="isError">
<h2 class="enter-pass__container__error__title">Encryption Passphrase Does not Match</h2>
<p class="enter-pass__container__error__message">
This passphrase hasnt yet been used in the browser. Please ensure this is the encryption passphrase
used in libulink or the Uplink CLI.
A previous fingerprint of a passphrase-based-key-derivation-function created in this browser doesn't
match the passphrase you just entered. Entering a passphrase not previously created will result in
the creation of a new passphrase.
</p>
<label class="enter-pass__container__error__check-area" :class="{ error: isCheckboxError }" for="error-checkbox">
<input
@ -84,11 +77,24 @@ import { MetaUtils } from '@/utils/meta';
},
})
export default class EnterPassphrase extends Vue {
private hashFromInput: string = '';
public passphrase: string = '';
public isError: boolean = false;
public isCheckboxChecked: boolean = false;
public isCheckboxError: boolean = false;
/**
* Lifecycle hook after initial render.
* Chooses correct route.
*/
public mounted(): void {
const idPassSalt: UserIDPassSalt | null = LocalData.getUserIDPassSalt();
if (!idPassSalt) {
this.$router.push({name: RouteConfig.CreatePassphrase.name});
}
}
/**
* Returns docs link from config.
*/
@ -99,38 +105,43 @@ export default class EnterPassphrase extends Vue {
/**
* Holds on access data button click logic.
*/
public onAccessDataClick(): void {
public async onAccessDataClick(): Promise<void> {
if (!this.passphrase) return;
const hashFromStorage: UserIDPassSalt | null = LocalData.getUserIDPassSalt();
if (!hashFromStorage) return;
pbkdf2.pbkdf2(this.passphrase, hashFromStorage.salt, 1, 64, (error, key) => {
if (error) return this.$notify.error(error.message);
const result: Buffer | Error = await this.pbkdf2Async(hashFromStorage.salt);
const hashFromInput: string = key.toString('hex');
const areHashesEqual = () => {
return hashFromStorage.passwordHash === hashFromInput;
};
if (result instanceof Error) {
await this.$notify.error(result.message);
switch (true) {
case areHashesEqual() ||
!areHashesEqual() && this.isError && this.isCheckboxChecked:
this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
this.$router.push({name: RouteConfig.BucketsManagement.name});
return;
}
return;
case !areHashesEqual() && this.isError && !this.isCheckboxChecked:
this.isCheckboxError = true;
this.hashFromInput = await result.toString('hex');
return;
case !areHashesEqual():
this.isError = true;
const areHashesEqual = () => {
return hashFromStorage.passwordHash === this.hashFromInput;
};
return;
default:
}
});
switch (true) {
case areHashesEqual() ||
!areHashesEqual() && this.isError && this.isCheckboxChecked:
await this.$store.dispatch(OBJECTS_ACTIONS.SET_PASSPHRASE, this.passphrase);
await this.$router.push({name: RouteConfig.BucketsManagement.name});
return;
case !areHashesEqual() && this.isError && !this.isCheckboxChecked:
this.isCheckboxError = true;
return;
case !areHashesEqual():
this.isError = true;
return;
default:
}
}
/**
@ -141,6 +152,20 @@ export default class EnterPassphrase extends Vue {
this.isCheckboxChecked = false;
this.isError = false;
}
/**
* Generates passphrase fingerprint asynchronously.
*/
private pbkdf2Async(salt: string): Promise<Buffer | Error> {
const ITERATIONS = 1;
const KEY_LENGTH = 64;
return new Promise((response, reject) => {
pbkdf2.pbkdf2(this.passphrase, salt, ITERATIONS, KEY_LENGTH, (error, key) => {
error ? reject(error) : response(key);
});
});
}
}
</script>
@ -212,12 +237,6 @@ export default class EnterPassphrase extends Vue {
line-height: 19px;
color: #1b2533;
margin: 10px 0 0 0;
&__link {
font-family: 'font_medium', sans-serif;
color: #0068dc;
text-decoration: underline;
}
}
}

View File

@ -11,39 +11,17 @@
import { Component, Vue } from 'vue-property-decorator';
import { RouteConfig } from '@/router';
import { LocalData, UserIDPassSalt } from '@/utils/localData';
import { MetaUtils } from '@/utils/meta';
@Component
export default class ObjectsArea extends Vue {
/**
* Lifecycle hook after initial render.
* Chooses correct route.
* Redirects if flow is disabled.
*/
public async mounted(): Promise<void> {
const DUPLICATE_NAV_ERROR: string = 'NavigationDuplicated';
const idPassSalt: UserIDPassSalt | null = LocalData.getUserIDPassSalt();
if (idPassSalt && idPassSalt.userId === this.$store.getters.user.id) {
try {
await this.$router.push(RouteConfig.Objects.with(RouteConfig.EnterPassphrase).path);
return;
} catch (error) {
if (error.name === DUPLICATE_NAV_ERROR) {
return;
}
await this.$notify.error(error.message);
}
}
try {
await this.$router.push(RouteConfig.Objects.with(RouteConfig.CreatePassphrase).path);
} catch (error) {
if (error.name === DUPLICATE_NAV_ERROR) {
return;
}
await this.$notify.error(error.message);
if (await JSON.parse(MetaUtils.getMetaContent('file-browser-flow-disabled'))) {
await this.$router.push(RouteConfig.ProjectDashboard.path);
}
}
}

View File

@ -1,9 +1,199 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="file-browser">
<FileBrowser></FileBrowser>
</div>
</template>
<script lang="ts">
import { FileBrowser } from 'browser';
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class UploadFile extends Vue {}
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { AccessGrant, GatewayCredentials } from '@/types/accessGrants';
import { MetaUtils } from '@/utils/meta';
@Component({
components: {
FileBrowser,
},
})
export default class UploadFile extends Vue {
private worker: Worker;
/**
* Lifecycle hook before initial render.
* Checks if bucket is chosen.
*/
public beforeMount(): void {
if (!this.bucket) {
this.$router.push(RouteConfig.Objects.with(RouteConfig.EnterPassphrase).path);
}
}
/**
* Lifecycle hook after initial render.
* Checks if bucket is chosen.
* Sets local worker.
*/
public mounted(): void {
if (!this.bucket) {
this.$router.push(RouteConfig.Objects.with(RouteConfig.EnterPassphrase).path);
return;
}
this.setWorker();
}
/**
* Lifecycle hook after vue instance was created.
* Initiates file browser.
*/
public created(): void {
if (!this.bucket) {
return;
}
this.$store.commit('files/init', {
endpoint: this.$store.state.objectsModule.gatewayCredentials.endpoint,
accessKey: this.$store.state.objectsModule.gatewayCredentials.accessKeyId,
secretKey: this.$store.state.objectsModule.gatewayCredentials.secretKey,
bucket: this.bucket,
browserRoot: RouteConfig.Objects.with(RouteConfig.UploadFile).path,
getObjectMapUrl: async (path: string) => await this.generateObjectMapUrl(path),
getSharedLink: async (path: string) => await this.generateShareLinkUrl(path),
});
}
/**
* Generates a URL for an object map.
*/
public async generateObjectMapUrl(path: string): Promise<string> {
path = `${this.bucket}/${path}`;
const now = new Date();
const inADay = new Date(now.setDate(now.getDate() + 1));
try {
const key: string = await this.accessKey(this.apiKey, inADay, path);
return `https://link.tardigradeshare.io/s/${key}/${path}?map=1`;
} catch (error) {
await this.$notify.error(error.message);
return '';
}
}
/**
* Generates a URL for a link sharing service.
*/
public async generateShareLinkUrl(path: string): Promise<string> {
path = `${this.bucket}/${path}`;
const now = new Date();
const notAfter = new Date(now.setFullYear(now.getFullYear() + 100));
const LINK_SHARING_AG_NAME = `${path}_shared-object_${now.toISOString()}`;
const cleanAPIKey: AccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, LINK_SHARING_AG_NAME);
try {
const key: string = await this.accessKey(cleanAPIKey.secret, notAfter, path);
return `https://link.tardigradeshare.io/${key}/${path}`;
} catch (error) {
await this.$notify.error(error.message);
return '';
}
}
/**
* 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);
};
}
/**
* Generates public access key.
*/
private async accessKey(cleanApiKey: string, notAfter: Date, path: string): Promise<string> {
const satelliteNodeURL = MetaUtils.getMetaContent('satellite-nodeurl');
this.worker.postMessage({
'type': 'GenerateAccess',
'apiKey': cleanApiKey,
'passphrase': this.passphrase,
'projectID': this.$store.getters.selectedProject.id,
'satelliteNodeURL': satelliteNodeURL,
});
const grantEvent: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
const grantData = grantEvent.data;
if (grantData.error) {
await this.$notify.error(grantData.error);
return '';
}
this.worker.postMessage({
'type': 'RestrictGrant',
'isDownload': true,
'isUpload': true,
'isList': true,
'isDelete': true,
'paths': [path],
'grant': grantData.value,
'notBefore': new Date().toISOString(),
'notAfter': notAfter.toISOString(),
});
const event: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
const data = event.data;
if (data.error) {
await this.$notify.error(data.error);
return '';
}
const gatewayCredentials: GatewayCredentials = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.GET_GATEWAY_CREDENTIALS, {accessGrant: data.value, isPublic: true});
return gatewayCredentials.accessKeyId;
}
/**
* Returns passphrase from store.
*/
private get passphrase(): string {
return this.$store.state.objectsModule.passphrase;
}
/**
* 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;
}
}
</script>
<style scoped>
@import '../../../node_modules/browser/dist/browser.css';
.file-browser {
font-family: 'font_regular', sans-serif;
padding-bottom: 200px;
}
</style>

View File

@ -388,6 +388,12 @@ router.beforeEach((to, from, next) => {
return;
}
if (navigateToDefaultSubTab(to.matched, RouteConfig.Objects)) {
next(RouteConfig.Objects.with(RouteConfig.CreatePassphrase).path);
return;
}
if (to.name === 'default') {
next(RouteConfig.ProjectDashboard.path);

View File

@ -1,6 +1,7 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { files } from 'browser';
import Vue from 'vue';
import Vuex from 'vuex';
@ -62,6 +63,7 @@ export const store = new Vuex.Store<ModulesState>({
projectsModule: makeProjectsModule(projectsApi),
bucketUsageModule: makeBucketsModule(bucketsApi),
objectsModule: makeObjectsModule(),
files,
},
});

View File

@ -215,7 +215,7 @@ export function makeAccessGrantsModule(api: AccessGrantsApi): StoreModule<Access
await api.deleteByNameAndProjectID(name, rootGetters.selectedProject.id);
},
getGatewayCredentials: async function({commit}: any, payload): Promise<GatewayCredentials> {
const credentials: GatewayCredentials = await api.getGatewayCredentials(payload.accessGrant, payload.optionalURL);
const credentials: GatewayCredentials = await api.getGatewayCredentials(payload.accessGrant, payload.optionalURL, payload.isPublic);
commit(SET_GATEWAY_CREDENTIALS, credentials);

View File

@ -1,23 +1,19 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
import S3, { Bucket } from 'aws-sdk/clients/s3';
import { StoreModule } from '@/store';
import { GatewayCredentials } from '@/types/accessGrants';
import {
Bucket,
CreateBucketCommand,
DeleteBucketCommand,
ListBucketsCommand,
ListBucketsOutput,
S3Client,
} from '@aws-sdk/client-s3';
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',
FETCH_BUCKETS: 'fetchBuckets',
CREATE_BUCKET: 'createBucket',
DELETE_BUCKET: 'deleteBucket',
@ -25,28 +21,34 @@ 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',
SET_FILE_COMPONENT_BUCKET_NAME: 'setFileComponentBucketName',
SET_PASSPHRASE: 'setPassphrase',
};
const {
CLEAR,
SET_API_KEY,
SET_ACCESS_GRANT,
SET_GATEWAY_CREDENTIALS,
SET_S3_CLIENT,
SET_BUCKETS,
SET_PASSPHRASE,
SET_FILE_COMPONENT_BUCKET_NAME,
} = OBJECTS_MUTATIONS;
export class ObjectsState {
public apiKey: string = '';
public accessGrant: string = '';
public gatewayCredentials: GatewayCredentials = new GatewayCredentials();
public s3Client: S3Client = new S3Client({});
public buckets: Bucket[] = [];
public s3Client: S3 = new S3({});
public bucketsList: Bucket[] = [];
public passphrase: string = '';
public fileComponentBucketName: string = '';
}
/**
@ -56,6 +58,9 @@ export function makeObjectsModule(): StoreModule<ObjectsState> {
return {
state: new ObjectsState(),
mutations: {
[SET_API_KEY](state: ObjectsState, apiKey: string) {
state.apiKey = apiKey;
},
[SET_ACCESS_GRANT](state: ObjectsState, accessGrant: string) {
state.accessGrant = accessGrant;
},
@ -63,36 +68,37 @@ export function makeObjectsModule(): StoreModule<ObjectsState> {
state.gatewayCredentials = credentials;
},
[SET_S3_CLIENT](state: ObjectsState) {
// TODO: use this for local testing. Remove after final implementation.
// state.gatewayCredentials.accessKeyId = 'jwitszrc76z4amjcrinv4zjpnlia';
// state.gatewayCredentials.secretKey = 'jyjufay7ddmwj6tlboyuj23yy4lqigfqa2ie25y526qmjj65khxzw';
// state.gatewayCredentials.endpoint = 'https://gateway.tardigradeshare.io';
const s3Config = {
credentials: {
accessKeyId: state.gatewayCredentials.accessKeyId,
secretAccessKey: state.gatewayCredentials.secretKey,
},
accessKeyId: state.gatewayCredentials.accessKeyId,
secretAccessKey: state.gatewayCredentials.secretKey,
endpoint: state.gatewayCredentials.endpoint,
s3ForcePathStyle: true,
signatureVersion: 'v4',
region: 'REGION',
};
state.s3Client = new S3Client(s3Config);
state.s3Client = new S3(s3Config);
},
[SET_BUCKETS](state: ObjectsState, buckets: Bucket[]) {
state.buckets = buckets;
state.bucketsList = buckets;
},
[SET_PASSPHRASE](state: ObjectsState, passphrase: string) {
state.passphrase = passphrase;
},
[SET_FILE_COMPONENT_BUCKET_NAME](state: ObjectsState, bucketName: string) {
state.fileComponentBucketName = bucketName;
},
[CLEAR](state: ObjectsState) {
state.apiKey = '';
state.passphrase = '';
state.accessGrant = '';
state.gatewayCredentials = new GatewayCredentials();
state.s3Client = new S3({});
state.bucketsList = [];
state.fileComponentBucketName = '';
},
},
actions: {
setApiKey: function({commit}: any, apiKey: string): void {
commit(SET_API_KEY, apiKey);
},
setAccessGrant: function({commit}: any, accessGrant: string): void {
commit(SET_ACCESS_GRANT, accessGrant);
},
@ -105,25 +111,23 @@ export function makeObjectsModule(): StoreModule<ObjectsState> {
setPassphrase: function({commit}: any, passphrase: string): void {
commit(SET_PASSPHRASE, passphrase);
},
setFileComponentBucketName: function({commit}: any, bucketName: string): void {
commit(SET_FILE_COMPONENT_BUCKET_NAME, bucketName);
},
fetchBuckets: async function(ctx): Promise<void> {
const bucketsOutput: ListBucketsOutput = await ctx.state.s3Client.send(new ListBucketsCommand({
credentials: {
accessKeyId: ctx.state.gatewayCredentials.accessKeyId,
secretAccessKey: ctx.state.gatewayCredentials.secretKey,
},
}));
const result = await ctx.state.s3Client.listBuckets().promise();
ctx.commit(SET_BUCKETS, bucketsOutput.Buckets);
ctx.commit(SET_BUCKETS, result.Buckets);
},
createBucket: async function(ctx, name: string): Promise<void> {
await ctx.state.s3Client.send(new CreateBucketCommand({
await ctx.state.s3Client.createBucket({
Bucket: name,
}));
}).promise();
},
deleteBucket: async function(ctx, name: string): Promise<void> {
await ctx.state.s3Client.send(new DeleteBucketCommand({
await ctx.state.s3Client.deleteBucket({
Bucket: name,
}));
}).promise();
},
clearObjects: function ({commit}: any): void {
commit(CLEAR);

View File

@ -47,7 +47,7 @@ export interface AccessGrantsApi {
* @returns GatewayCredentials
* @throws Error
*/
getGatewayCredentials(accessGrant: string, optionalURL?: string): Promise<GatewayCredentials>;
getGatewayCredentials(accessGrant: string, optionalURL?: string, isPublic?: boolean): Promise<GatewayCredentials>;
}
/**

View File

@ -53,7 +53,7 @@ self.onmessage = function (event) {
permission.NotBefore = notBefore;
permission.NotAfter = notAfter;
if (data.type == "SetPermission") {
if (data.type === "SetPermission") {
const buckets = data.buckets;
apiKey = data.apiKey;
result = self.setAPIKeyPermission(apiKey, buckets, permission);