satellite/console: configure sending invites to unregistered emails

This change adds a flag to the satellite config indicating whether
unregistered email addresses should receive project invitation emails.

Change-Id: I0396f25574ddae3f9adaea32a6e7cd15b931bf12
This commit is contained in:
Jeremy Wharton 2023-10-17 05:08:19 -05:00
parent 24ae79345b
commit f8b59a50ff
13 changed files with 93 additions and 27 deletions

View File

@ -14,19 +14,20 @@ import (
// Config keeps track of core console service configuration parameters.
type Config struct {
PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"`
OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"`
DefaultProjectLimit int `help:"default project limits for users" default:"1" testDefault:"5"`
AsOfSystemTimeDuration time.Duration `help:"default duration for AS OF SYSTEM TIME" devDefault:"-5m" releaseDefault:"-5m" testDefault:"0"`
LoginAttemptsWithoutPenalty int `help:"number of times user can try to login without penalty" default:"3"`
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"`
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/"`
UsageLimits UsageLimitsConfig
Captcha CaptchaConfig
Session SessionConfig
PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"`
OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"`
DefaultProjectLimit int `help:"default project limits for users" default:"1" testDefault:"5"`
AsOfSystemTimeDuration time.Duration `help:"default duration for AS OF SYSTEM TIME" devDefault:"-5m" releaseDefault:"-5m" testDefault:"0"`
LoginAttemptsWithoutPenalty int `help:"number of times user can try to login without penalty" default:"3"`
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"`
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/"`
UsageLimits UsageLimitsConfig
Captcha CaptchaConfig
Session SessionConfig
}
// CaptchaConfig contains configurations for login/registration captcha system.

View File

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

View File

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

View File

@ -3821,7 +3821,7 @@ func (s *Service) inviteProjectMembers(ctx context.Context, sender *User, projec
}
}
unverifiedUsers = append(unverifiedUsers, oldest)
} else {
} else if s.config.UnregisteredInviteEmailsEnabled {
newUserEmails = append(newUserEmails, email)
}
}

View File

@ -400,6 +400,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# url link to terms and conditions page
# console.terms-and-conditions-url: https://www.storj.io/terms-of-service/
# indicates whether invitation emails can be sent to unregistered email addresses
# console.unregistered-invite-emails-enabled: false
# the default free-tier bandwidth usage limit
# console.usage-limits.bandwidth.free: 25.00 GB

View File

@ -61,7 +61,7 @@
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue';
import { computed, h, ref } from 'vue';
import { Validator } from '@/utils/validation';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
@ -71,6 +71,7 @@ import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useLoading } from '@/composables/useLoading';
import { MODALS } from '@/utils/constants/appStatePopUps';
@ -86,6 +87,8 @@ const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const configStore = useConfigStore();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
@ -135,7 +138,18 @@ async function onPrimaryClick(): Promise<void> {
}
analyticsStore.eventTriggered(AnalyticsEvent.PROJECT_MEMBERS_INVITE_SENT);
notify.notify('Invite sent!');
if (configStore.state.config.unregisteredInviteEmailsEnabled) {
notify.notify('Invite sent!');
} else {
notify.notify(() => [
h('p', { class: 'message-title' }, 'Invite sent!'),
h('p', { class: 'message-info' }, [
'An invitation will be sent to the email address if it belongs to a user on this satellite.',
]),
]);
}
pmStore.setSearchQuery('');
try {

View File

@ -73,7 +73,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { computed, onMounted, onBeforeUnmount, ref, h } from 'vue';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
@ -168,7 +168,17 @@ async function resendInvites(): Promise<void> {
try {
await pmStore.reinviteMembers(pmStore.state.selectedProjectMembersEmails, projectsStore.state.selectedProject.id);
notify.success('Invites re-sent!');
if (configStore.state.config.unregisteredInviteEmailsEnabled) {
notify.success('Invites re-sent!');
} else {
notify.success(() => [
h('p', { class: 'message-title' }, 'Invites re-sent!'),
h('p', { class: 'message-info' }, [
'Invitations will be re-sent to the email addresses that belong to users on this satellite.',
]),
]);
}
} catch (error) {
error.message = `Unable to resend project invitations. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER);

View File

@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { computed, h, onMounted, ref } from 'vue';
import { ProjectMemberItemModel } from '@/types/projectMembers';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
@ -57,6 +57,7 @@ import { useLoading } from '@/composables/useLoading';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useConfigStore } from '@/store/modules/configStore';
import VLoader from '@/components/common/VLoader.vue';
import HeaderArea from '@/components/team/HeaderArea.vue';
@ -69,6 +70,7 @@ const analyticsStore = useAnalyticsStore();
const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
const configStore = useConfigStore();
const notify = useNotify();
const { withLoading } = useLoading();
@ -153,7 +155,18 @@ function onResendClick(member: ProjectMemberItemModel) {
analyticsStore.eventTriggered(AnalyticsEvent.RESEND_INVITE_CLICKED);
try {
await pmStore.reinviteMembers([member.getEmail()], projectsStore.state.selectedProject.id);
notify.notify('Invite resent!');
if (configStore.state.config.unregisteredInviteEmailsEnabled) {
notify.success('Invite re-sent!');
} else {
notify.success(() => [
h('p', { class: 'message-title' }, 'Invites re-sent!'),
h('p', { class: 'message-info' }, [
'The invitation will be re-sent to the email address if it belongs to a user on this satellite.',
]),
]);
}
pmStore.setSearchQuery('');
} catch (error) {
error.message = `Error resending invite. ${error.message}`;

View File

@ -57,11 +57,12 @@ export const useNotificationsStore = defineStore('notifications', () => {
addNotification(notification);
}
function notifyInfo(message: NotificationMessage): void {
function notifyInfo(message: NotificationMessage, title?: string): void {
const notification = new DelayedNotification(
() => deleteNotification(notification.id),
NotificationType.Info,
message,
title,
);
addNotification(notification);

View File

@ -13,7 +13,7 @@ export class FrontendConfig {
satelliteName: string;
satelliteNodeURL: string;
stripePublicKey: string;
partneredSatellites: PartneredSatellite[];
partneredSatellites?: PartneredSatellite[];
defaultProjectLimit: number;
generalRequestURL: string;
projectLimitsIncreaseRequestURL: string;
@ -48,6 +48,7 @@ export class FrontendConfig {
neededTransactionConfirmations: number;
objectBrowserPaginationEnabled: boolean;
billingFeaturesEnabled: boolean;
unregisteredInviteEmailsEnabled: boolean;
}
export class MultiCaptchaConfig {
@ -55,6 +56,7 @@ export class MultiCaptchaConfig {
hcaptcha: SingleCaptchaConfig;
}
// TODO: This class was added manually because TypeScript generation is broken.
export class PartneredSatellite {
name: string;
address: string;

View File

@ -38,9 +38,9 @@ export class Notificator {
notificationsStore.notifyError(message, source);
}
public notify(message: NotificationMessage): void {
public notify(message: NotificationMessage, title?: string): void {
const notificationsStore = useNotificationsStore();
notificationsStore.notifyInfo(message);
notificationsStore.notifyInfo(message, title);
}
public warning(message: NotificationMessage): void {

View File

@ -157,6 +157,7 @@ import { useNotify } from '@/utils/hooks';
import { Project } from '@/types/projects';
import { SortDirection, tableSizeOptions } from '@/types/common';
import { useUsersStore } from '@/store/modules/usersStore';
import { useConfigStore } from '@/store/modules/configStore';
import IconTrash from '@poc/components/icons/IconTrash.vue';
import IconCopy from '@poc/components/icons/IconCopy.vue';
@ -174,9 +175,9 @@ type RenderedItem = {
const usersStore = useUsersStore();
const analyticsStore = useAnalyticsStore();
const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
const configStore = useConfigStore();
const router = useRouter();
const notify = useNotify();
@ -304,7 +305,14 @@ async function resendInvite(email: string): Promise<void> {
analyticsStore.eventTriggered(AnalyticsEvent.RESEND_INVITE_CLICKED);
try {
await pmStore.reinviteMembers([email], selectedProject.value.id);
notify.notify('Invite resent!');
if (configStore.state.config.unregisteredInviteEmailsEnabled) {
notify.notify('Invite re-sent!');
} else {
notify.notify(
'The invitation will be re-sent to the email address if it belongs to a user on this satellite.',
'Invite re-sent!',
);
}
} catch (error) {
error.message = `Error resending invite. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.PROJECT_MEMBERS_PAGE);

View File

@ -131,6 +131,7 @@ import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useNotify } from '@/utils/hooks';
import { useLoading } from '@/composables/useLoading';
import { useUsersStore } from '@/store/modules/usersStore';
import { useConfigStore } from '@/store/modules/configStore';
import UpgradeAccountDialog from '@poc/components/dialogs/upgradeAccountFlow/UpgradeAccountDialog.vue';
@ -151,6 +152,8 @@ const model = computed<boolean>({
const usersStore = useUsersStore();
const analyticsStore = useAnalyticsStore();
const pmStore = useProjectMembersStore();
const configStore = useConfigStore();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
@ -184,7 +187,16 @@ async function onPrimaryClick(): Promise<void> {
await withLoading(async () => {
try {
await pmStore.inviteMember(email.value, props.projectId);
notify.success('Invite sent!');
if (configStore.state.config.unregisteredInviteEmailsEnabled) {
notify.success('Invite sent!');
} else {
notify.success(
'An invitation will be sent to the email address if it belongs to a user on this satellite.',
'Invite sent!',
);
}
email.value = '';
} catch (error) {
error.message = `Error inviting project member. ${error.message}`;