Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CORL-3103]: Make protected email domain bans customizable #4550

Merged
merged 6 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions client/src/core/client/admin/components/BanModal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
replaceHistoryLocation,
} from "coral-framework/testHelpers";

import { PROTECTED_EMAIL_DOMAINS } from "coral-common/common/lib/constants";
import { pureMerge } from "coral-common/common/lib/utils";
import {
GQLNEW_USER_MODERATION,
Expand Down Expand Up @@ -250,7 +249,7 @@ it("does not display ban domain option for moderated domain", async () => {
});

it("does not display ban domain option for protected domain", async () => {
const protectedDomain = PROTECTED_EMAIL_DOMAINS.values().next().value;
const protectedDomain = "gmail.com";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a nice cleanup, makes the tests clearer, thanks!

const protectedEmailResolver = createResolversStub<GQLResolver>({
Query: {
users: () => ({
Expand Down
5 changes: 3 additions & 2 deletions client/src/core/client/admin/components/BanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Form } from "react-final-form";
import { graphql } from "react-relay";

import NotAvailable from "coral-admin/components/NotAvailable";
import { PROTECTED_EMAIL_DOMAINS } from "coral-common/common/lib/constants";
import { extractDomain } from "coral-common/common/lib/email";
import {
isOrgModerator,
Expand Down Expand Up @@ -77,6 +76,7 @@ interface Props {
emailDomainModeration: UserStatusChangeContainer_settings["emailDomainModeration"];
userRole: string;
isMultisite: boolean;
protectedEmailDomains: ReadonlyArray<string>;
}

interface BanButtonProps {
Expand Down Expand Up @@ -141,6 +141,7 @@ const BanModal: FunctionComponent<Props> = ({
userBanStatus,
userRole,
isMultisite,
protectedEmailDomains,
}) => {
const createDomainBan = useMutation(BanDomainMutation);
const banUser = useMutation(BanUserMutation);
Expand Down Expand Up @@ -233,7 +234,7 @@ const BanModal: FunctionComponent<Props> = ({
updateType !== UpdateType.NO_SITES &&
emailDomain &&
!domainIsConfigured &&
!PROTECTED_EMAIL_DOMAINS.has(emailDomain);
!protectedEmailDomains.includes(emailDomain);

useEffect(() => {
if (viewerIsSingleSiteMod) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
userEmail={comment.author.email}
userRole={comment.author.role}
isMultisite={settings.multisite}
protectedEmailDomains={settings.protectedEmailDomains}
/>
)}
</>
Expand Down Expand Up @@ -530,6 +531,7 @@ const enhanced = withFragmentContainer<Props>({
`,
settings: graphql`
fragment ModerateCardContainer_settings on Settings {
protectedEmailDomains
locale
wordList {
banned
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ const UserStatusChangeContainer: FunctionComponent<Props> = ({
emailDomainModeration={settings.emailDomainModeration}
userBanStatus={user.status.ban}
userRole={user.role}
protectedEmailDomains={settings.protectedEmailDomains}
/>
)}
</>
Expand Down Expand Up @@ -286,6 +287,7 @@ const enhanced = withFragmentContainer<Props>({
newUserModeration
}
multisite
protectedEmailDomains
}
`,
viewer: graphql`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.textArea {
height: 150px;
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent, useState } from "react";
import { Field } from "react-final-form";
import { graphql } from "relay-runtime";

import { formatStringList, parseStringList } from "coral-framework/lib/form";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { validateEmailDomainList } from "coral-framework/lib/validation";
import { AddIcon, ButtonSvgIcon } from "coral-ui/components/icons";
import { Button, Flex, FormFieldDescription } from "coral-ui/components/v2";
import {
Button,
Flex,
FormField,
FormFieldDescription,
FormFieldHeader,
HelperText,
Label,
Textarea,
} from "coral-ui/components/v2";

import { EmailDomainConfigContainer_settings } from "coral-admin/__generated__/EmailDomainConfigContainer_settings.graphql";

import ConfigBox from "../../ConfigBox";
import Header from "../../Header";

import ValidationMessage from "../../ValidationMessage";
import EmailDomainTableContainer from "./EmailDomainTableContainer";

import styles from "./EmailDomainConfigContainer.css";

interface Props {
settings: EmailDomainConfigContainer_settings;
disabled: boolean;
}

const EmailDomainConfigContainer: FunctionComponent<Props> = ({ settings }) => {
// eslint-disable-next-line no-unused-expressions
graphql`
fragment EmailDomainConfigContainer_formValues on Settings {
protectedEmailDomains
}
`;

const EmailDomainConfigContainer: FunctionComponent<Props> = ({
settings,
disabled,
}) => {
const { protectedEmailDomains } = settings;
const [showDomainList, setShowDomainList] = useState(false);

return (
Expand All @@ -33,8 +59,7 @@ const EmailDomainConfigContainer: FunctionComponent<Props> = ({ settings }) => {
<Localized id="configure-moderation-emailDomains-description">
<FormFieldDescription>
Create rules to take action on accounts or comments based on the
account holder's email address domain. Action only applies to newly
created accounts.
account holder's email address domain.
</FormFieldDescription>
</Localized>
<Localized
Expand All @@ -61,13 +86,51 @@ const EmailDomainConfigContainer: FunctionComponent<Props> = ({ settings }) => {
)}
</Flex>
{showDomainList && <EmailDomainTableContainer settings={settings} />}
<FormField>
<FormFieldHeader>
<Localized id="configure-moderation-emailDomains-exceptions-header">
<Label component="legend">Exceptions</Label>
</Localized>
</FormFieldHeader>
<Localized id="configure-moderation-emailDomains-exceptions-helperText">
<HelperText>
These domains cannot be banned. Domains should be written without
www, for example `gmail.com`. Separate domains with a comma.
</HelperText>
</Localized>
<Field
name="protectedEmailDomains"
parse={parseStringList}
format={formatStringList}
validate={validateEmailDomainList}
defaultValue={protectedEmailDomains}
>
{({ input, meta }) => (
<>
<Textarea
{...input}
className={styles.textArea}
id={`configure-advanced-${input.name}`}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
fullwidth
disabled={disabled}
/>
<ValidationMessage meta={meta} />
</>
)}
</Field>
</FormField>
</ConfigBox>
);
};

const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment EmailDomainConfigContainer_settings on Settings {
protectedEmailDomains
...EmailDomainTableContainer_settings
}
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
font-family: var(--font-family-primary);
font-size: var(--font-size-3);
}

.textArea {
height: 150px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ModerationConfigContainer: React.FunctionComponent<Props> = ({
<RecentCommentHistoryConfig disabled={submitting} />
<ExternalLinksConfigContainer disabled={submitting} settings={settings} />
<PremoderateEmailAddressConfig disabled={submitting} />
<EmailDomainConfigContainer settings={settings} />
<EmailDomainConfigContainer disabled={submitting} settings={settings} />
</HorizontalGutter>
);
};
Expand All @@ -67,6 +67,7 @@ const enhanced = withFragmentContainer<Props>({
...NewCommentersConfigContainer_formValues @relay(mask: false)
...NewCommentersConfigContainer_settings
...EmailDomainConfigContainer_settings
...EmailDomainConfigContainer_formValues @relay(mask: false)
...ExternalLinksConfigContainer_formValues @relay(mask: false)
...ExternalLinksConfigContainer_settings
...PremoderateEmailAddressConfig_formValues @relay(mask: false)
Expand Down
2 changes: 2 additions & 0 deletions client/src/core/client/admin/test/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
DEFAULT_SESSION_DURATION,
PROTECTED_EMAIL_DOMAINS,
TOXICITY_THRESHOLD_DEFAULT,
} from "coral-common/common/lib/constants";
import TIME from "coral-common/common/lib/time";
Expand Down Expand Up @@ -240,6 +241,7 @@ export const settings = createFixture<GQLSettings>({
method: GQLDSA_METHOD_OF_REDRESS.NONE,
},
},
protectedEmailDomains: Array.from(PROTECTED_EMAIL_DOMAINS),
});

export const settingsWithMultisite = createFixture<GQLSettings>(
Expand Down
19 changes: 18 additions & 1 deletion client/src/core/client/framework/lib/validation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,24 @@ export const validatePercentage = (min: number, max: number) =>
export const validateDeleteConfirmation = (phrase: string) =>
createValidator((v) => v === phrase, DELETE_CONFIRMATION_INVALID());

export const validateEmailDomainList = createValidator((v) => {
if (!Array.isArray(v)) {
return false;
}

for (const domain of v) {
if (typeof domain !== "string") {
return false;
}

if (!EMAIL_DOMAIN_REGEX.test(domain)) {
return false;
}
}

return true;
}, INVALID_EMAIL_DOMAIN());

export const validateStrictURLList = createValidator((v) => {
if (!Array.isArray(v)) {
return false;
Expand Down Expand Up @@ -355,7 +373,6 @@ export type Condition<T = any, V = any> = (value: T, values: V) => boolean;
/**
* composeSomeConditions will return true when some of the conditions return
* true, false if all return false.
*
* @param conditions conditions to use
*/
export function composeSomeConditions<T = any, V = any>(
Expand Down
4 changes: 3 additions & 1 deletion locales/en-US/admin.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ configure-moderation-newCommenters-comments = comments

#### Email domain
configure-moderation-emailDomains-header = Email domain
configure-moderation-emailDomains-description = Create rules to take action on accounts or comments based on the account holder's email address domain. Action only applies to newly created accounts.
configure-moderation-emailDomains-description = Create rules to take action on accounts or comments based on the account holder's email address domain.
configure-moderation-emailDomains-add = Add email domain
configure-moderation-emailDomains-edit = Edit email domain
configure-moderation-emailDomains-addDomain = <icon></icon> Add domain
Expand All @@ -890,6 +890,8 @@ configure-moderation-emailDomains-form-editDomain = Update
configure-moderation-emailDomains-confirmDelete = Deleting this email domain will stop any new accounts created with it from being banned or always pre-moderated. Are you sure you want to continue?
configure-moderation-emailDomains-form-description-add = Add a domain and select the action that should be taken when on every new account created using the specified domain.
configure-moderation-emailDomains-form-description-edit = Update the domain or action that should be taken when on every new account using the specified domain.
configure-moderation-emailDomains-exceptions-header = Exceptions
configure-moderation-emailDomains-exceptions-helperText = These domains cannot be banned. Domains should be written without www, for example `gmail.com`. Separate domains with a comma.

configure-moderation-emailDomains-showCurrent = Show current domain list
configure-moderation-emailDomains-hideCurrent = Hide current domain list
Expand Down
4 changes: 4 additions & 0 deletions server/src/core/server/graph/resolvers/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PROTECTED_EMAIL_DOMAINS } from "coral-common/common/lib/constants";
import {
defaultDSAConfiguration,
defaultRTEConfiguration,
Expand Down Expand Up @@ -79,6 +80,9 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
return flairBadges;
},
dsa: ({ dsa = defaultDSAConfiguration }) => dsa,
protectedEmailDomains: ({
protectedEmailDomains = Array.from(PROTECTED_EMAIL_DOMAINS),
}) => protectedEmailDomains,
inPageNotifications: ({
inPageNotifications = { enabled: true, floatingBellIndicator: true },
}) => inPageNotifications,
Expand Down
12 changes: 12 additions & 0 deletions server/src/core/server/graph/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,12 @@ type Settings @cacheControl(maxAge: 30) {
"""
emailDomainModeration: [EmailDomain!]!

"""
protectedEmailDomains is the configuration for email domains that are protected from email
domain moderation rules such as all accounts banned
"""
protectedEmailDomains: [String!]!

"""
externalProfileURL is a string template for a link to a user's
external profile.
Expand Down Expand Up @@ -6728,6 +6734,12 @@ input SettingsInput {
"""
premoderateEmailAddress: PremoderateEmailAddressConfigurationInput

"""
protectedEmailDomains is the configuration for email domains that are protected from email
domain moderation rules such as all accounts banned
"""
protectedEmailDomains: [String!]

"""
inPageNotifications specifies the configuration for in-page notifications
"""
Expand Down
6 changes: 6 additions & 0 deletions server/src/core/server/models/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ export type Settings = GlobalModerationSettings &
*/
topCommenter?: TopCommenterConfig;

/**
* protectedEmailDomains is the configuration for email domains that are protected from email
* domain moderation rules such as all accounts banned
*/
protectedEmailDomains: string[];

/**
* inPageNotifications specifies whether or not in-page notifications are enabled
* as an option for commenters
Expand Down
6 changes: 5 additions & 1 deletion server/src/core/server/models/tenant/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { isEmpty } from "lodash";
import { DateTime } from "luxon";
import { v4 as uuid } from "uuid";

import { DEFAULT_SESSION_DURATION } from "coral-common/common/lib/constants";
import {
DEFAULT_SESSION_DURATION,
PROTECTED_EMAIL_DOMAINS,
} from "coral-common/common/lib/constants";
import { LanguageCode } from "coral-common/common/lib/helpers/i18n/locales";
import TIME from "coral-common/common/lib/time";
import { DeepPartial, Sub } from "coral-common/common/lib/types";
Expand Down Expand Up @@ -303,6 +306,7 @@ export const combineTenantDefaultsAndInput = (
topCommenter: {
enabled: false,
},
protectedEmailDomains: Array.from(PROTECTED_EMAIL_DOMAINS),
inPageNotifications: {
enabled: true,
floatingBellIndicator: true,
Expand Down
3 changes: 1 addition & 2 deletions server/src/core/server/services/tenant/tenant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createEmailDomain } from "./tenant";
jest.mock("coral-server/models/user");
jest.mock("coral-server/models/site");

import { PROTECTED_EMAIL_DOMAINS } from "coral-common/common/lib/constants";
import { UserForbiddenError } from "coral-server/errors";
import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types";
import {
Expand Down Expand Up @@ -67,7 +66,7 @@ it("does not create domain bans for protected domains", async () => {
tenantID: tenant.id,
role: GQLUSER_ROLE.ADMIN,
});
const protectedDomain = PROTECTED_EMAIL_DOMAINS.values().next().value;
const protectedDomain = "gmail.com";
await expect(async () =>
createEmailDomain(mockMongo, mockRedis, mockTenantCache, tenant, viewer, {
domain: protectedDomain,
Expand Down
Loading
Loading