satellite/console,web/satellite: configure whether free user can invite

This change adds a flag to the satellite config indicating whether
free tier users should be able to send project invitations.

Change-Id: I9c030c88dbef136ba4a9bf2d8f027a8dcd77fd33
This commit is contained in:
Jeremy Wharton 2023-10-18 14:19:40 -05:00 committed by Storj Robot
parent e5fd061e70
commit a6222afdd0
10 changed files with 38 additions and 43 deletions

View File

@ -22,6 +22,7 @@ type Config struct {
FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"`
ProjectInvitationExpiration time.Duration `help:"duration that project member invitations are valid for" default:"168h"`
UnregisteredInviteEmailsEnabled bool `help:"indicates whether invitation emails can be sent to unregistered email addresses" default:"false"`
FreeTierInvitesEnabled bool `help:"indicates whether free tier users can send project invitations" default:"false"`
UserBalanceForUpgrade int64 `help:"amount of base units of US micro dollars needed to upgrade user's tier status" default:"10000000"`
PlacementEdgeURLOverrides PlacementEdgeURLOverrides `help:"placement-specific edge service URL overrides in the format {\"placementID\": {\"authService\": \"...\", \"publicLinksharing\": \"...\", \"internalLinksharing\": \"...\"}, \"placementID2\": ...}"`
BlockExplorerURL string `help:"url of the transaction block explorer" default:"https://etherscan.io/"`

View File

@ -52,6 +52,7 @@ type FrontendConfig struct {
ObjectBrowserPaginationEnabled bool `json:"objectBrowserPaginationEnabled"`
BillingFeaturesEnabled bool `json:"billingFeaturesEnabled"`
UnregisteredInviteEmailsEnabled bool `json:"unregisteredInviteEmailsEnabled"`
FreeTierInvitesEnabled bool `json:"freeTierInvitesEnabled"`
}
// Satellites is a configuration value that contains a list of satellite names and addresses.

View File

@ -753,6 +753,7 @@ func (server *Server) frontendConfigHandler(w http.ResponseWriter, r *http.Reque
ObjectBrowserPaginationEnabled: server.config.ObjectBrowserPaginationEnabled,
BillingFeaturesEnabled: server.config.BillingFeaturesEnabled,
UnregisteredInviteEmailsEnabled: server.config.UnregisteredInviteEmailsEnabled,
FreeTierInvitesEnabled: server.config.FreeTierInvitesEnabled,
}
err := json.NewEncoder(w).Encode(&cfg)

View File

@ -3775,7 +3775,7 @@ func (s *Service) inviteProjectMembers(ctx context.Context, sender *User, projec
}
projectID = isMember.project.ID
if !sender.PaidTier {
if !(s.config.FreeTierInvitesEnabled || sender.PaidTier) {
return nil, ErrNotPaidTier.New(paidTierInviteErrMsg)
}

View File

@ -280,6 +280,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# allow domains to embed the satellite in a frame, space separated
# console.frame-ancestors: tardigrade.io storj.io
# indicates whether free tier users can send project invitations
# console.free-tier-invites-enabled: false
# server address of the front-end app
# console.frontend-address: :10200

View File

@ -8,21 +8,21 @@
<div class="modal__header">
<TeamMembersIcon />
<h1 class="modal__header__title">
{{ isPaidTier ? 'Invite team member' : 'Upgrade to Pro' }}
{{ needsUpgrade ? 'Upgrade to Pro' : 'Invite team member' }}
</h1>
</div>
<p class="modal__info">
<template v-if="isPaidTier">
Add a team member to contribute to this project.
<template v-if="needsUpgrade">
Upgrade now to unlock collaboration and bring your team together in this project.
</template>
<template v-else>
Upgrade now to unlock collaboration and bring your team together in this project.
Add a team member to contribute to this project.
</template>
</p>
<VInput
v-if="isPaidTier"
v-if="!needsUpgrade"
class="modal__input"
label="Email"
height="38px"
@ -43,14 +43,14 @@
:on-press="closeModal"
/>
<VButton
:label="isPaidTier ? 'Invite' : 'Upgrade'"
:label="needsUpgrade ? 'Upgrade' : 'Invite'"
height="48px"
font-size="14px"
border-radius="10px"
:on-press="onPrimaryClick"
:is-disabled="!!formError || isLoading"
>
<template v-if="!isPaidTier" #icon-right>
<template v-if="needsUpgrade" #icon-right>
<ArrowIcon />
</template>
</VButton>
@ -101,7 +101,7 @@ const email = ref<string>('');
* or a message describing the validation error.
*/
const formError = computed<string | boolean>(() => {
if (!isPaidTier.value) return false;
if (needsUpgrade.value) return false;
if (!email.value) return true;
if (email.value.toLocaleLowerCase() === usersStore.state.user.email.toLowerCase()) {
return `You can't add yourself to the project.`;
@ -113,17 +113,17 @@ const formError = computed<string | boolean>(() => {
});
/**
* Returns user's paid tier status from store.
* Returns whether the user should upgrade to pro tier before inviting.
*/
const isPaidTier = computed<boolean>(() => {
return usersStore.state.user.paidTier;
const needsUpgrade = computed<boolean>(() => {
return !(usersStore.state.user.paidTier || configStore.state.config.freeTierInvitesEnabled);
});
/**
* Handles primary button click.
*/
async function onPrimaryClick(): Promise<void> {
if (!isPaidTier.value) {
if (needsUpgrade.value) {
appStore.updateActiveModal(MODALS.upgradeAccount);
return;
}

View File

@ -49,6 +49,7 @@ export class FrontendConfig {
objectBrowserPaginationEnabled: boolean;
billingFeaturesEnabled: boolean;
unregisteredInviteEmailsEnabled: boolean;
freeTierInvitesEnabled: boolean;
}
export class MultiCaptchaConfig {

View File

@ -2,6 +2,8 @@
// See LICENSE for copying information.
<template>
<v-overlay v-model="model" persistent />
<v-dialog
:model-value="model && !isUpgradeDialogShown"
width="auto"
@ -18,7 +20,7 @@
</template>
<v-card-title class="font-weight-bold">
{{ isPaidTier ? 'Add Member' : 'Upgrade to Pro' }}
{{ needsUpgrade ? 'Upgrade to Pro' : 'Add Member' }}
</v-card-title>
<template #append>
@ -37,7 +39,10 @@
<v-form v-model="valid" class="pa-7 pb-4" @submit.prevent="onPrimaryClick">
<v-row>
<template v-if="isPaidTier">
<v-col v-if="needsUpgrade">
Upgrade now to unlock collaboration and bring your team together in this project.
</v-col>
<template v-else>
<v-col cols="12">
<p class="mb-5">Invite a team member to join you in this project.</p>
<v-alert
@ -63,9 +68,6 @@
/>
</v-col>
</template>
<v-col v-else>
Upgrade now to unlock collaboration and bring your team together in this project.
</v-col>
</v-row>
</v-form>
@ -82,10 +84,10 @@
variant="flat"
block
:loading="isLoading"
:append-icon="!isPaidTier ? 'mdi-arrow-right' : undefined"
:append-icon="needsUpgrade ? 'mdi-arrow-right' : undefined"
@click="onPrimaryClick"
>
{{ isPaidTier ? 'Send Invite' : 'Upgrade' }}
{{ needsUpgrade ? 'Upgrade' : 'Send Invite' }}
</v-btn>
</v-col>
</v-row>
@ -98,12 +100,6 @@
:model-value="model && isUpgradeDialogShown"
@update:model-value="v => model = isUpgradeDialogShown = v"
/>
<teleport to="body">
<v-fade-transition>
<div v-show="model" class="v-overlay__scrim custom-scrim" />
</v-fade-transition>
</teleport>
</template>
<script setup lang="ts">
@ -121,7 +117,7 @@ import {
VAlert,
VTextField,
VCardActions,
VFadeTransition,
VOverlay,
} from 'vuetify/components';
import { RequiredRule, ValidationRule } from '@poc/types/common';
@ -167,17 +163,17 @@ const emailRules: ValidationRule<string>[] = [
];
/**
* Returns user's paid tier status from store.
* Returns whether the user should upgrade to pro tier before inviting.
*/
const isPaidTier = computed<boolean>(() => {
return usersStore.state.user.paidTier;
const needsUpgrade = computed<boolean>(() => {
return !(usersStore.state.user.paidTier || configStore.state.config.freeTierInvitesEnabled);
});
/**
* Handles primary button click.
*/
async function onPrimaryClick(): Promise<void> {
if (!isPaidTier.value) {
if (needsUpgrade.value) {
isUpgradeDialogShown.value = true;
return;
}

View File

@ -2,6 +2,8 @@
// See LICENSE for copying information.
<template>
<v-overlay v-model="model" persistent />
<v-dialog
:model-value="model && !isUpgradeDialogShown"
width="410px"
@ -113,16 +115,10 @@
:model-value="model && isUpgradeDialogShown"
@update:model-value="v => model = isUpgradeDialogShown = v"
/>
<teleport to="body">
<v-fade-transition>
<div v-show="model" class="v-overlay__scrim custom-scrim" />
</v-fade-transition>
</teleport>
</template>
<script setup lang="ts">
import { ref, computed, watch, Teleport } from 'vue';
import { ref, computed, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
VDialog,
@ -136,7 +132,7 @@ import {
VRow,
VCol,
VTextField,
VFadeTransition,
VOverlay,
} from 'vuetify/components';
import { RequiredRule, ValidationRule } from '@poc/types/common';

View File

@ -122,10 +122,6 @@ html {
opacity: 0.75;
}
.custom-scrim {
z-index: 2000;
}
// Align the checkboxes in the tables
.v-selection-control {
contain: inherit;