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:
parent
24ae79345b
commit
f8b59a50ff
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
3
scripts/testdata/satellite-config.yaml.lock
vendored
3
scripts/testdata/satellite-config.yaml.lock
vendored
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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}`;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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}`;
|
||||
|
Loading…
Reference in New Issue
Block a user