web/satellite: enable session timeout setting
This change adds a new setting on the account settings page to change session timeout duration. The old settings page is replaced with a new one used on the all projects dashboard page. Also, onboarding API endpoints and store action have been changed to be generic to include session timeout setting. Issue: https://github.com/storj/storj/issues/5560 Change-Id: I9026e61c6f86e4be5f9357e5d20e280eab2c29ea
This commit is contained in:
parent
857a292e52
commit
6f8dff5832
@ -4,9 +4,17 @@
|
||||
import { ErrorBadRequest } from '@/api/errors/ErrorBadRequest';
|
||||
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
|
||||
import { ErrorTooManyRequests } from '@/api/errors/ErrorTooManyRequests';
|
||||
import { TokenInfo, UpdatedUser, User, UsersApi, UserSettings } from '@/types/users';
|
||||
import {
|
||||
SetUserSettingsData,
|
||||
TokenInfo,
|
||||
UpdatedUser,
|
||||
User,
|
||||
UsersApi,
|
||||
UserSettings,
|
||||
} from '@/types/users';
|
||||
import { HttpClient } from '@/utils/httpClient';
|
||||
import { ErrorTokenExpired } from '@/api/errors/ErrorTokenExpired';
|
||||
import { Duration } from '@/utils/time';
|
||||
|
||||
/**
|
||||
* AuthHttpApi is a console Auth API.
|
||||
@ -260,16 +268,27 @@ export class AuthHttpApi implements UsersApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes user's onboarding status.
|
||||
* Changes user's settings.
|
||||
*
|
||||
* @param data
|
||||
* @returns UserSettings
|
||||
* @throws Error
|
||||
*/
|
||||
public async setOnboardingStatus(status: Partial<UserSettings>): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/account/onboarding`;
|
||||
const response = await this.http.patch(path, JSON.stringify(status));
|
||||
if (!response.ok) {
|
||||
throw new Error('can not set onboarding status');
|
||||
public async updateSettings(data: SetUserSettingsData): Promise<UserSettings> {
|
||||
const path = `${this.ROOT_PATH}/account/settings`;
|
||||
const response = await this.http.patch(path, JSON.stringify(data));
|
||||
if (response.ok) {
|
||||
const responseData = await response.json();
|
||||
|
||||
return new UserSettings(
|
||||
responseData.sessionDuration,
|
||||
responseData.onboardingStart,
|
||||
responseData.onboardingEnd,
|
||||
responseData.onboardingStep,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('can not get user settings');
|
||||
}
|
||||
|
||||
// TODO: remove secret after Vanguard release
|
||||
|
@ -23,8 +23,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings__section__content__divider" />
|
||||
|
||||
<div class="settings__section__content__row">
|
||||
<span class="settings__section__content__row__title">Email</span>
|
||||
<span class="settings__section__content__row__subtitle">{{ user.email }}</span>
|
||||
@ -51,10 +49,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings__section__content__divider" />
|
||||
|
||||
<div class="settings__section__content__row">
|
||||
<span class="settings__section__content__row__title">Two-Factor Authentication</span>
|
||||
<div class="settings__section__content__row__title">
|
||||
<p>Two-Factor</p>
|
||||
<p>Authentication</p>
|
||||
</div>
|
||||
<span v-if="!user.isMFAEnabled" class="settings__section__content__row__subtitle">Improve account security by enabling 2FA.</span>
|
||||
<span v-else class="settings__section__content__row__subtitle">2FA is enabled.</span>
|
||||
<div class="settings__section__content__row__actions">
|
||||
@ -78,6 +77,22 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings__section__content__row">
|
||||
<span class="settings__section__content__row__title">Session Timeout</span>
|
||||
<span v-if="userDuration" class="settings__section__content__row__subtitle">{{ userDuration.shortString }} of inactivity will log you out.</span>
|
||||
<span v-else class="settings__section__content__row__subtitle">Duration of inactivity that will log you out.</span>
|
||||
<div class="settings__section__content__row__actions">
|
||||
<VButton
|
||||
class="button"
|
||||
is-white
|
||||
font-size="14px"
|
||||
width="100px"
|
||||
:on-press="toggleEditSessionTimeoutModal"
|
||||
label="Set Timeout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -88,19 +103,15 @@ import { computed, onMounted } from 'vue';
|
||||
|
||||
import { USER_ACTIONS } from '@/store/modules/users';
|
||||
import { User } from '@/types/users';
|
||||
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
import { useNotify, useStore } from '@/utils/hooks';
|
||||
import { useLoading } from '@/composables/useLoading';
|
||||
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
|
||||
import { Duration } from '@/utils/time';
|
||||
|
||||
import VButton from '@/components/common/VButton.vue';
|
||||
|
||||
import ChangePasswordIcon from '@/../static/images/account/profile/changePassword.svg';
|
||||
import EmailIcon from '@/../static/images/account/profile/email.svg';
|
||||
import EditIcon from '@/../static/images/common/edit.svg';
|
||||
|
||||
const store = useStore();
|
||||
const notify = useNotify();
|
||||
const { isLoading, withLoading } = useLoading();
|
||||
@ -112,6 +123,13 @@ const user = computed((): User => {
|
||||
return store.getters.user;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns duration from store.
|
||||
*/
|
||||
const userDuration = computed((): Duration | null => {
|
||||
return store.state.usersModule.settings.sessionDuration;
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles enable MFA modal visibility.
|
||||
*/
|
||||
@ -140,6 +158,13 @@ function toggleChangePasswordModal(): void {
|
||||
store.commit(APP_STATE_MUTATIONS.UPDATE_ACTIVE_MODAL, MODALS.changePassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens edit session timeout modal.
|
||||
*/
|
||||
function toggleEditSessionTimeoutModal(): void {
|
||||
store.commit(APP_STATE_MUTATIONS.UPDATE_ACTIVE_MODAL, MODALS.editSessionTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens edit account info modal.
|
||||
*/
|
||||
@ -201,19 +226,14 @@ onMounted(() => {
|
||||
|
||||
&__content {
|
||||
margin-top: 20px;
|
||||
background: var(--c-white);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
|
||||
&__divider {
|
||||
margin: 10px;
|
||||
border: 0.5px solid var(--c-grey-2);
|
||||
}
|
||||
|
||||
&__row {
|
||||
padding: 10px;
|
||||
min-height: 55px;
|
||||
background: var(--c-white);
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 4%);
|
||||
border-radius: 8px;
|
||||
padding: 10px 30px;
|
||||
margin-bottom: 20px;
|
||||
height: 88px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
@ -225,6 +245,7 @@ onMounted(() => {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
&__title {
|
||||
|
@ -18,14 +18,7 @@
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<span class="label" :class="{uppercase: isUppercase}">
|
||||
<CopyIcon v-if="icon.toLowerCase() === 'copy'" />
|
||||
<LockIcon v-if="icon.toLowerCase() === 'lock'" />
|
||||
<CreditCardIcon v-if="icon.toLowerCase() === 'credit-card'" />
|
||||
<DocumentIcon v-if="icon.toLowerCase() === 'document'" />
|
||||
<TrashIcon v-if="icon.toLowerCase() === 'trash'" />
|
||||
<FolderIcon v-if="icon.toLowerCase() === 'folder'" />
|
||||
<resources-icon v-if="icon.toLowerCase() === 'resources'" />
|
||||
<add-circle-icon v-if="icon.toLowerCase() === 'addcircle'" />
|
||||
<component :is="iconComponent" v-if="iconComponent" />
|
||||
<span v-if="icon !== 'none'"> </span>
|
||||
{{ label }}
|
||||
</span>
|
||||
@ -46,15 +39,7 @@
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<span class="label" :class="{uppercase: isUppercase}">
|
||||
<CopyIcon v-if="icon.toLowerCase() === 'copy'" />
|
||||
<DownloadIcon v-if="icon.toLowerCase() === 'download'" />
|
||||
<LockIcon v-if="icon.toLowerCase() === 'lock'" />
|
||||
<CreditCardIcon v-if="icon.toLowerCase() === 'credit-card'" />
|
||||
<DocumentIcon v-if="icon.toLowerCase() === 'document'" />
|
||||
<TrashIcon v-if="icon.toLowerCase() === 'trash'" />
|
||||
<FolderIcon v-if="icon.toLowerCase() === 'folder'" />
|
||||
<resources-icon v-if="icon.toLowerCase() === 'resources'" />
|
||||
<add-circle-icon v-if="icon.toLowerCase() === 'addcircle'" />
|
||||
<component :is="iconComponent" v-if="iconComponent" />
|
||||
<span v-if="icon !== 'none'"> </span>
|
||||
{{ label }}
|
||||
</span>
|
||||
@ -66,7 +51,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { computed, VueConstructor } from 'vue';
|
||||
|
||||
import AddCircleIcon from '@/../static/images/common/addCircle.svg';
|
||||
import CopyIcon from '@/../static/images/common/copyButtonIcon.svg';
|
||||
@ -120,6 +105,20 @@ const props = withDefaults(defineProps<{
|
||||
onPress: () => {},
|
||||
});
|
||||
|
||||
const icons = new Map<string, VueConstructor>([
|
||||
['copy', CopyIcon],
|
||||
['download', DownloadIcon],
|
||||
['lock', LockIcon],
|
||||
['credit-card', CreditCardIcon],
|
||||
['document', DocumentIcon],
|
||||
['trash', TrashIcon],
|
||||
['folder', FolderIcon],
|
||||
['resources', ResourcesIcon],
|
||||
['addcircle', AddCircleIcon],
|
||||
]);
|
||||
|
||||
const iconComponent = computed((): VueConstructor | undefined => icons.get(props.icon.toLowerCase()));
|
||||
|
||||
const containerClassName = computed((): string => {
|
||||
if (props.isDisabled) return 'disabled';
|
||||
|
||||
|
205
web/satellite/src/components/modals/EditSessionTimeoutModal.vue
Normal file
205
web/satellite/src/components/modals/EditSessionTimeoutModal.vue
Normal file
@ -0,0 +1,205 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<VModal :on-close="withLoading(onClose)">
|
||||
<template #content>
|
||||
<div class="timeout-modal">
|
||||
<div class="timeout-modal__header">
|
||||
<Icon class="timeout-modal__header__icon" />
|
||||
<h1 class="timeout-modal__header__title">
|
||||
Session Timeout
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="timeout-modal__divider" />
|
||||
|
||||
<p class="timeout-modal__info">Select your session timeout duration.</p>
|
||||
|
||||
<div class="timeout-modal__divider" />
|
||||
|
||||
<p class="timeout-modal__label">Session timeout duration</p>
|
||||
<timeout-selector :selected="sessionDuration" @select="durationChange" />
|
||||
|
||||
<div class="timeout-modal__divider" />
|
||||
|
||||
<div class="timeout-modal__buttons">
|
||||
<VButton
|
||||
label="Cancel"
|
||||
width="100%"
|
||||
height="40px"
|
||||
border-radius="10px"
|
||||
font-size="13px"
|
||||
is-white
|
||||
class="timeout-modal__buttons__button"
|
||||
:on-press="withLoading(onClose)"
|
||||
:is-disabled="isLoading"
|
||||
/>
|
||||
<VButton
|
||||
label="Save"
|
||||
width="100%"
|
||||
height="40px"
|
||||
border-radius="10px"
|
||||
font-size="13px"
|
||||
class="timeout-modal__buttons__button save"
|
||||
:on-press="withLoading(save)"
|
||||
:is-disabled="isLoading || !hasChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
|
||||
import { useNotify, useStore } from '@/utils/hooks';
|
||||
import { Duration } from '@/utils/time';
|
||||
import { USER_ACTIONS } from '@/store/modules/users';
|
||||
import { SetUserSettingsData } from '@/types/users';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
|
||||
import VButton from '@/components/common/VButton.vue';
|
||||
import VModal from '@/components/common/VModal.vue';
|
||||
import TimeoutSelector from '@/components/modals/editSessionTimeout/TimeoutSelector.vue';
|
||||
|
||||
import Icon from '@/../static/images/session/inactivityTimer.svg';
|
||||
|
||||
const store = useStore();
|
||||
const notify = useNotify();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const sessionDuration = ref<Duration | null>(null);
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Make the current selected duration the already configured one.
|
||||
*/
|
||||
onMounted(() => {
|
||||
sessionDuration.value = userDuration.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns duration from store.
|
||||
*/
|
||||
const userDuration = computed((): Duration | null => {
|
||||
return store.state.usersModule.settings.sessionDuration;
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether the user has changed this setting.
|
||||
*/
|
||||
const hasChanged = computed((): boolean => {
|
||||
if (!sessionDuration.value) {
|
||||
return false;
|
||||
}
|
||||
return !userDuration.value?.isEqualTo(sessionDuration.value as Duration);
|
||||
});
|
||||
|
||||
/**
|
||||
* durationChange is called when the user selects a different duration.
|
||||
* @param duration the user's selection.
|
||||
* */
|
||||
function durationChange(duration: Duration) {
|
||||
sessionDuration.value = duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* save submits the changed duration.
|
||||
* */
|
||||
async function save() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await store.dispatch(USER_ACTIONS.UPDATE_SETTINGS, {
|
||||
sessionDuration: sessionDuration.value?.nanoseconds ?? 0,
|
||||
} as SetUserSettingsData);
|
||||
notify.success(`Session timeout changed successfully. Your session timeout is ${sessionDuration.value?.shortString}.`);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
await notify.error(error.message, AnalyticsErrorEventSource.EDIT_TIMEOUT_MODAL);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onClose is called to close this modal.
|
||||
* */
|
||||
async function onClose() {
|
||||
store.commit(APP_STATE_MUTATIONS.REMOVE_ACTIVE_MODAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that disables modal interaction during execution.
|
||||
*/
|
||||
function withLoading(fn: () => Promise<void>): () => Promise<void> {
|
||||
return async () => {
|
||||
if (isLoading.value) return;
|
||||
isLoading.value = true;
|
||||
await fn();
|
||||
isLoading.value = false;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.timeout-modal {
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
text-align: left;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
margin: 20px 0;
|
||||
border: 1px solid var(--c-grey-2);
|
||||
}
|
||||
|
||||
&__info {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
&__button {
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,167 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="selector">
|
||||
<div v-click-outside="closeSelector" tabindex="0" class="selector__content" @keyup.enter="toggleSelector" @click="toggleSelector">
|
||||
<span v-if="selected" class="selector__content__label">{{ selected?.shortString }}</span>
|
||||
<span v-else class="selector__content__label">Select duration</span>
|
||||
<arrow-down-icon class="selector__content__arrow" :class="{ open: isOpen }" />
|
||||
</div>
|
||||
<div v-if="isOpen" class="selector__dropdown">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index" tabindex="0"
|
||||
class="selector__dropdown__item"
|
||||
:class="{ selected: isSelected(option) }"
|
||||
@click.stop="() => select(option)"
|
||||
@keyup.enter="() => select(option)"
|
||||
>
|
||||
<span class="selector__dropdown__item__label">{{ option.shortString }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { useStore } from '@/utils/hooks';
|
||||
import { APP_STATE_DROPDOWNS } from '@/utils/constants/appStatePopUps';
|
||||
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
|
||||
import { Duration } from '@/utils/time';
|
||||
|
||||
import ArrowDownIcon from '@/../static/images/common/dropIcon.svg';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps<{
|
||||
selected: Duration | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
// select is emitted when the selection changes.
|
||||
(e: 'select', duration: Duration): void
|
||||
}>();
|
||||
|
||||
const options = [
|
||||
Duration.MINUTES_15,
|
||||
Duration.MINUTES_30,
|
||||
Duration.HOUR_1,
|
||||
Duration.DAY_1,
|
||||
Duration.WEEK_1,
|
||||
Duration.DAY_30,
|
||||
];
|
||||
|
||||
/**
|
||||
* whether the selector drop down is open
|
||||
* */
|
||||
const isOpen = computed((): boolean => {
|
||||
return store.state.appStateModule.viewsState.activeDropdown === APP_STATE_DROPDOWNS.TIMEOUT_SELECTOR;
|
||||
});
|
||||
|
||||
/**
|
||||
* whether an option is currently selected.
|
||||
* @param option
|
||||
* */
|
||||
function isSelected(option: Duration): boolean {
|
||||
if (!props.selected) {
|
||||
return false;
|
||||
}
|
||||
return props.selected.isEqualTo(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* select sends the new selection to a parent component.
|
||||
* @param option the new selection
|
||||
* */
|
||||
function select(option: Duration) {
|
||||
emit('select', option);
|
||||
closeSelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* closeSelector closes the selector dropdown.
|
||||
* */
|
||||
function closeSelector() {
|
||||
store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
|
||||
}
|
||||
|
||||
/**
|
||||
* toggleSelector closes or opens the selector dropdown
|
||||
* */
|
||||
function toggleSelector() {
|
||||
if (isOpen.value) {
|
||||
store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
|
||||
} else {
|
||||
store.commit(APP_STATE_ACTIONS.TOGGLE_ACTIVE_DROPDOWN, APP_STATE_DROPDOWNS.TIMEOUT_SELECTOR);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.selector {
|
||||
border: 1px solid var(--c-grey-3);
|
||||
border-radius: 6px;
|
||||
max-width: 170px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
margin: 10px 14px;
|
||||
|
||||
&__label {
|
||||
font-family: 'font_regular', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--c-grey-6);
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
transition-duration: 0.5s;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg) scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
background: var(--c-white);
|
||||
z-index: 999;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 -2px 16px rgb(0 0 0 / 10%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--c-grey-2);
|
||||
width: 100%;
|
||||
|
||||
&__item {
|
||||
padding: 10px;
|
||||
|
||||
&.selected {
|
||||
background: var(--c-grey-1);
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-top-right-radius: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--c-grey-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -87,7 +87,7 @@ async function onUploadInBrowserClick(): Promise<void> {
|
||||
|
||||
async function endOnboarding(): Promise<void> {
|
||||
try {
|
||||
await store.dispatch(USER_ACTIONS.SET_ONBOARDING_STATUS, {
|
||||
await store.dispatch(USER_ACTIONS.UPDATE_SETTINGS, {
|
||||
onboardingEnd: true,
|
||||
} as Partial<UserSettings>);
|
||||
} catch (error) {
|
||||
@ -102,7 +102,7 @@ async function endOnboarding(): Promise<void> {
|
||||
onMounted(async (): Promise<void> => {
|
||||
try {
|
||||
if (!store.state.usersModule.settings.onboardingStart) {
|
||||
await store.dispatch(USER_ACTIONS.SET_ONBOARDING_STATUS, {
|
||||
await store.dispatch(USER_ACTIONS.UPDATE_SETTINGS, {
|
||||
onboardingStart: true,
|
||||
} as Partial<UserSettings>);
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ onBeforeMount(async () => {
|
||||
|
||||
if (!store.state.usersModule.settings.onboardingStart) {
|
||||
try {
|
||||
await store.dispatch(USER_ACTIONS.SET_ONBOARDING_STATUS, {
|
||||
await store.dispatch(USER_ACTIONS.UPDATE_SETTINGS, {
|
||||
onboardingStart: true,
|
||||
} as Partial<UserSettings>);
|
||||
} catch (error) {
|
||||
|
@ -66,7 +66,7 @@ async function onFinishClick(): Promise<void> {
|
||||
|
||||
async function endOnboarding(): Promise<void> {
|
||||
try {
|
||||
await store.dispatch(USER_ACTIONS.SET_ONBOARDING_STATUS, {
|
||||
await store.dispatch(USER_ACTIONS.UPDATE_SETTINGS, {
|
||||
onboardingEnd: true,
|
||||
} as Partial<UserSettings>);
|
||||
} catch (error) {
|
||||
|
@ -204,7 +204,7 @@ export const router = new Router({
|
||||
{
|
||||
path: RouteConfig.Settings.path,
|
||||
name: RouteConfig.Settings.name,
|
||||
component: SettingsArea,
|
||||
component: NewSettingsArea,
|
||||
},
|
||||
{
|
||||
path: RouteConfig.Billing.path,
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import {
|
||||
DisableMFARequest,
|
||||
SetUserSettingsData,
|
||||
UpdatedUser,
|
||||
User,
|
||||
UsersApi,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
} from '@/types/users';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
import { StoreModule } from '@/types/store';
|
||||
import { Duration } from '@/utils/time';
|
||||
|
||||
export const USER_ACTIONS = {
|
||||
LOGIN: 'loginUser',
|
||||
@ -22,7 +24,7 @@ export const USER_ACTIONS = {
|
||||
CLEAR: 'clearUser',
|
||||
GET_FROZEN_STATUS: 'getFrozenStatus',
|
||||
GET_SETTINGS: 'getSettings',
|
||||
SET_ONBOARDING_STATUS: 'setOnboardingStatus',
|
||||
UPDATE_SETTINGS: 'updateSettings',
|
||||
};
|
||||
|
||||
export const USER_MUTATIONS = {
|
||||
@ -51,7 +53,7 @@ const {
|
||||
GENERATE_USER_MFA_RECOVERY_CODES,
|
||||
GET_FROZEN_STATUS,
|
||||
GET_SETTINGS,
|
||||
SET_ONBOARDING_STATUS,
|
||||
UPDATE_SETTINGS,
|
||||
} = USER_ACTIONS;
|
||||
|
||||
const {
|
||||
@ -142,12 +144,8 @@ export function makeUsersModule(api: UsersApi): StoreModule<UsersState, UsersCon
|
||||
|
||||
return settings;
|
||||
},
|
||||
[SET_ONBOARDING_STATUS]: async function ({ commit, state }: UsersContext, status: Partial<UserSettings>): Promise<void> {
|
||||
await api.setOnboardingStatus(status);
|
||||
const settings = state.settings;
|
||||
for (const statusKey in status) {
|
||||
settings[statusKey] = status[statusKey];
|
||||
}
|
||||
[UPDATE_SETTINGS]: async function ({ commit, state }: UsersContext, update: SetUserSettingsData): Promise<void> {
|
||||
const settings = await api.updateSettings(update);
|
||||
|
||||
commit(SET_SETTINGS, settings);
|
||||
},
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { Duration } from '@/utils/time';
|
||||
|
||||
/**
|
||||
* Exposes all user-related functionality.
|
||||
*/
|
||||
@ -36,12 +38,13 @@ export interface UsersApi {
|
||||
getUserSettings(): Promise<UserSettings>;
|
||||
|
||||
/**
|
||||
* Changes user's onboarding status.
|
||||
* Changes user's settings.
|
||||
*
|
||||
* @param data
|
||||
* @returns UserSettings
|
||||
* @throws Error
|
||||
*/
|
||||
setOnboardingStatus(status: Partial<UserSettings>): Promise<void>;
|
||||
updateSettings(data: SetUserSettingsData): Promise<UserSettings>;
|
||||
|
||||
/**
|
||||
* Enable user's MFA.
|
||||
@ -157,16 +160,23 @@ export class TokenInfo {
|
||||
*/
|
||||
export class UserSettings {
|
||||
public constructor(
|
||||
private _sessionDuration: string | null = null,
|
||||
private _sessionDuration: number | null = null,
|
||||
public onboardingStart = false,
|
||||
public onboardingEnd = false,
|
||||
public onboardingStep: string | null = null,
|
||||
) {}
|
||||
|
||||
public get sessionDuration(): Date | null {
|
||||
public get sessionDuration(): Duration | null {
|
||||
if (this._sessionDuration) {
|
||||
return new Date(this._sessionDuration);
|
||||
return new Duration(this._sessionDuration);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SetUserSettingsData {
|
||||
onboardingStart?: boolean
|
||||
onboardingEnd?: boolean;
|
||||
onboardingStep?: string | null;
|
||||
sessionDuration?: number;
|
||||
}
|
||||
|
@ -123,4 +123,5 @@ export enum AnalyticsErrorEventSource {
|
||||
ALL_PROJECT_DASHBOARD = 'All projects dashboard error',
|
||||
ONBOARDING_OVERVIEW_STEP = 'Onboarding Overview step error',
|
||||
PRICING_PLAN_STEP = 'Onboarding Pricing Plan step error',
|
||||
EDIT_TIMEOUT_MODAL = 'Edit session timeout error',
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import ObjectDetailsModal from '@/components/modals/ObjectDetailsModal.vue';
|
||||
import EnterPassphraseModal from '@/components/modals/EnterPassphraseModal.vue';
|
||||
import PricingPlanModal from '@/components/modals/PricingPlanModal.vue';
|
||||
import NewCreateProjectModal from '@/components/modals/NewCreateProjectModal.vue';
|
||||
import EditSessionTimeoutModal from '@/components/modals/EditSessionTimeoutModal.vue';
|
||||
|
||||
export const APP_STATE_DROPDOWNS = {
|
||||
ACCOUNT: 'isAccountDropdownShown',
|
||||
@ -44,6 +45,7 @@ export const APP_STATE_DROPDOWNS = {
|
||||
CHART_DATE_PICKER: 'isChartsDatePickerShown',
|
||||
PERMISSIONS: 'isPermissionsDropdownShown',
|
||||
PAYMENT_SELECTION: 'isPaymentSelectionShown',
|
||||
TIMEOUT_SELECTOR: 'timeoutSelector',
|
||||
};
|
||||
|
||||
enum Modals {
|
||||
@ -72,6 +74,7 @@ enum Modals {
|
||||
ENTER_PASSPHRASE = 'enterPassphrase',
|
||||
PRICING_PLAN = 'pricingPlan',
|
||||
NEW_CREATE_PROJECT = 'newCreateProject',
|
||||
EDIT_SESSION_TIMEOUT = 'editSessionTimeout',
|
||||
}
|
||||
|
||||
// modals could be of VueConstructor type or Object (for composition api components).
|
||||
@ -101,4 +104,5 @@ export const MODALS: Record<Modals, unknown> = {
|
||||
[Modals.ENTER_PASSPHRASE]: EnterPassphraseModal,
|
||||
[Modals.PRICING_PLAN]: PricingPlanModal,
|
||||
[Modals.NEW_CREATE_PROJECT]: NewCreateProjectModal,
|
||||
[Modals.EDIT_SESSION_TIMEOUT]: EditSessionTimeoutModal,
|
||||
};
|
||||
|
@ -13,3 +13,80 @@ export class Time {
|
||||
return Math.floor(time.getTime() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class simplifies working with duration (nanoseconds) sent from the backend.
|
||||
* */
|
||||
export class Duration {
|
||||
|
||||
static MINUTES_15 = new Duration(9e+11);
|
||||
static MINUTES_30 = new Duration(1.8e+12);
|
||||
static HOUR_1 = new Duration(3.6e+12);
|
||||
static DAY_1 = new Duration(8.64e+13);
|
||||
static WEEK_1 = new Duration(6.048e+14);
|
||||
static DAY_30 = new Duration(2.592e+15);
|
||||
|
||||
public constructor(
|
||||
public nanoseconds: number,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* parsed returns an object of the time components in this.nanoseconds
|
||||
* */
|
||||
get parsed(): { days: number, hours: number; seconds: number; minutes: number } {
|
||||
const seconds = Math.floor((this.nanoseconds / 1000000) / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
return {
|
||||
days,
|
||||
hours: hours % 24,
|
||||
minutes: minutes % 60,
|
||||
seconds: seconds % 60,
|
||||
};
|
||||
}
|
||||
|
||||
get days(): number {
|
||||
return this.parsed.days;
|
||||
}
|
||||
|
||||
get hours(): number {
|
||||
return this.parsed.hours;
|
||||
}
|
||||
|
||||
get minutes(): number {
|
||||
return this.parsed.minutes;
|
||||
}
|
||||
|
||||
get seconds(): number {
|
||||
return this.parsed.seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* shortString represents this duration in the appropriate unit.
|
||||
* */
|
||||
get shortString(): string {
|
||||
let numberPart = this.seconds;
|
||||
let unitPart = 'second';
|
||||
if (this.days > 0) {
|
||||
numberPart = this.days;
|
||||
unitPart = 'day';
|
||||
} else if (this.hours > 0) {
|
||||
numberPart = this.hours;
|
||||
unitPart = 'hour';
|
||||
} if (this.minutes > 0) {
|
||||
numberPart = this.minutes;
|
||||
unitPart = 'minute';
|
||||
}
|
||||
|
||||
if (numberPart > 1) {
|
||||
unitPart = `${unitPart}s`;
|
||||
}
|
||||
return `${numberPart} ${unitPart}`;
|
||||
}
|
||||
|
||||
public isEqualTo(other: Duration): boolean {
|
||||
return this.nanoseconds === other.nanoseconds;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { UpdatedUser, User, UsersApi, UserSettings } from '@/types/users';
|
||||
import {
|
||||
SetUserSettingsData,
|
||||
UpdatedUser,
|
||||
User,
|
||||
UsersApi,
|
||||
UserSettings,
|
||||
} from '@/types/users';
|
||||
|
||||
/**
|
||||
* Mock for UsersApi
|
||||
@ -25,8 +31,8 @@ export class UsersApiMock implements UsersApi {
|
||||
return Promise.resolve(new UserSettings());
|
||||
}
|
||||
|
||||
public setOnboardingStatus(status: Partial<UserSettings>): Promise<void> {
|
||||
return Promise.resolve();
|
||||
public updateSettings(status: SetUserSettingsData): Promise<UserSettings> {
|
||||
return Promise.resolve(new UserSettings());
|
||||
}
|
||||
|
||||
public update(_user: UpdatedUser): Promise<void> {
|
||||
|
Loading…
Reference in New Issue
Block a user