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"` 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"` 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"` 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"` 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\": ...}"` 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/"` 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"` ObjectBrowserPaginationEnabled bool `json:"objectBrowserPaginationEnabled"`
BillingFeaturesEnabled bool `json:"billingFeaturesEnabled"` BillingFeaturesEnabled bool `json:"billingFeaturesEnabled"`
UnregisteredInviteEmailsEnabled bool `json:"unregisteredInviteEmailsEnabled"` UnregisteredInviteEmailsEnabled bool `json:"unregisteredInviteEmailsEnabled"`
FreeTierInvitesEnabled bool `json:"freeTierInvitesEnabled"`
} }
// Satellites is a configuration value that contains a list of satellite names and addresses. // 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, ObjectBrowserPaginationEnabled: server.config.ObjectBrowserPaginationEnabled,
BillingFeaturesEnabled: server.config.BillingFeaturesEnabled, BillingFeaturesEnabled: server.config.BillingFeaturesEnabled,
UnregisteredInviteEmailsEnabled: server.config.UnregisteredInviteEmailsEnabled, UnregisteredInviteEmailsEnabled: server.config.UnregisteredInviteEmailsEnabled,
FreeTierInvitesEnabled: server.config.FreeTierInvitesEnabled,
} }
err := json.NewEncoder(w).Encode(&cfg) 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 projectID = isMember.project.ID
if !sender.PaidTier { if !(s.config.FreeTierInvitesEnabled || sender.PaidTier) {
return nil, ErrNotPaidTier.New(paidTierInviteErrMsg) 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 # allow domains to embed the satellite in a frame, space separated
# console.frame-ancestors: tardigrade.io storj.io # 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 # server address of the front-end app
# console.frontend-address: :10200 # console.frontend-address: :10200

View File

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

View File

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

View File

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

View File

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

View File

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