[CORL-439] Allow admins to configure GDPR features (#2497)

* allow tenant admins to modify commenter account features

* style account features configuration

* prevent changing username or downloading comments based on tenant settings

* update fixtures and snaps

* add i18n strings

* update snap
This commit is contained in:
Tessa Thornton
2019-09-03 16:56:36 -06:00
committed by Kim Gardner
parent 46f0d08cf7
commit fcf3640adc
16 changed files with 1817 additions and 1233 deletions
@@ -15,6 +15,7 @@ interface Props {
offLabel?: React.ReactNode;
format?: ((value: any, name: string) => any) | null;
parse?: ((value: any, name: string) => any) | null;
className?: string;
}
const OnOffField: FunctionComponent<Props> = ({
@@ -25,8 +26,9 @@ const OnOffField: FunctionComponent<Props> = ({
invert = false,
parse = parseStringBool,
format,
className,
}) => (
<div>
<div className={className}>
<Field
name={name}
type="radio"
@@ -0,0 +1,8 @@
.radioButtons {
display: flex;
padding-left: var(--spacing-4);
align-items: flex-start;
& > *:not(:last-child) {
margin-right: var(--spacing-5);
}
}
@@ -0,0 +1,140 @@
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import {
FieldSet,
Flex,
FormField,
HorizontalGutter,
InputLabel,
Typography,
} from "coral-ui/components";
import OnOffField from "../../OnOffField";
import Header from "../../Header";
import styles from "./AccountFeaturesConfig.css";
interface Props {
disabled?: boolean;
}
const AccountFeaturesConfig: FunctionComponent<Props> = ({ disabled }) => (
<div>
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-account-features-title">
<Header container="legend">Commenter account mangement features</Header>
</Localized>
<Localized id="configure-account-features-explanation">
<Typography variant="bodyCopy">
You can enable and disable certain features for your commenters to use
within their Profile. These features also assist towards GDPR
compliance.
</Typography>
</Localized>
<HorizontalGutter container={<FieldSet />}>
<Localized id="configure-account-features-allow">
<Typography variant="heading4">Allow users to:</Typography>
</Localized>
<FormField container="fieldset">
<Flex justifyContent="space-between">
<HorizontalGutter size="half">
<Localized id="configure-account-features-change-usernames">
<InputLabel container="legend">
Change their usernames
</InputLabel>
</Localized>
<Localized id="configure-account-features-change-usernames-details">
<Typography>
Usernames can be changed once every 14 days.
</Typography>
</Localized>
</HorizontalGutter>
<OnOffField
name="accountFeatures.changeUsername"
disabled={disabled || false}
className={styles.radioButtons}
onLabel={
<Localized id="configure-account-features-yes">
<span>Yes</span>
</Localized>
}
offLabel={
<Localized id="configure-account-features-no">
<span>No</span>
</Localized>
}
/>
</Flex>
</FormField>
</HorizontalGutter>
<HorizontalGutter size="double">
<FormField container="fieldset">
<Flex justifyContent="space-between">
<HorizontalGutter size="half">
<Localized id="configure-account-features-download-comments">
<InputLabel container="legend">
Download their comments
</InputLabel>
</Localized>
<Localized id="configure-account-features-download-comments-details">
<Typography>
Commenters can download a csv of their comment history.
</Typography>
</Localized>
</HorizontalGutter>
<OnOffField
name="accountFeatures.downloadComments"
disabled={disabled || false}
className={styles.radioButtons}
onLabel={
<Localized id="configure-account-features-yes">
<span>Yes</span>
</Localized>
}
offLabel={
<Localized id="configure-account-features-no">
<span>No</span>
</Localized>
}
/>
</Flex>
</FormField>
</HorizontalGutter>
<HorizontalGutter size="double">
<FormField container="fieldset">
<Flex justifyContent="space-between">
<HorizontalGutter size="half">
<Localized id="configure-account-features-delete-account">
<InputLabel container="legend">Delete their account</InputLabel>
</Localized>
<Localized id="configure-account-features-delete-account-details">
<Typography>
Removes all of their comment data, username, and email address
from the site and the database.
</Typography>
</Localized>
</HorizontalGutter>
<OnOffField
name="accountFeatures.deleteAccount"
disabled={disabled || false}
className={styles.radioButtons}
onLabel={
<Localized id="configure-account-features-yes">
<span>Yes</span>
</Localized>
}
offLabel={
<Localized id="configure-account-features-no">
<span>No</span>
</Localized>
}
/>
</Flex>
</FormField>
</HorizontalGutter>
</HorizontalGutter>
</div>
);
export default AccountFeaturesConfig;
@@ -0,0 +1,44 @@
import React from "react";
import { graphql } from "react-relay";
import { AccountFeaturesConfigContainer_settings as SettingsData } from "coral-admin/__generated__/AccountFeaturesConfigContainer_settings.graphql";
import { DeepNullable, DeepPartial } from "coral-common/types";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { GQLSettings } from "coral-framework/schema";
import AccountFeaturesConfig from "./AccountFeaturesConfig";
export type FormProps = DeepNullable<Pick<GQLSettings, "accountFeatures">>;
export type OnInitValuesFct = (values: DeepPartial<FormProps>) => void;
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled?: boolean;
}
class AccountFeaturesConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <AccountFeaturesConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment AccountFeaturesConfigContainer_settings on Settings {
accountFeatures {
changeUsername
deleteAccount
downloadComments
}
}
`,
})(AccountFeaturesConfigContainer);
export default enhanced;
@@ -5,6 +5,7 @@ import React from "react";
import { graphql } from "react-relay";
import { AuthConfigContainer_auth as AuthData } from "coral-admin/__generated__/AuthConfigContainer_auth.graphql";
import { AuthConfigContainer_settings as SettingsData } from "coral-admin/__generated__/AuthConfigContainer_settings.graphql";
import { DeepNullable, DeepPartial } from "coral-common/types";
import { pureMerge } from "coral-common/utils";
import { CoralContext, withContext } from "coral-framework/lib/bootstrap";
@@ -17,10 +18,14 @@ import {
import { getMessage } from "coral-framework/lib/i18n";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { GQLSettings } from "coral-framework/schema";
import { HorizontalGutter } from "coral-ui/components";
import AccountFeaturesConfigContainer from "./AccountFeaturesConfigContainer";
import AuthConfig from "./AuthConfig";
export type FormProps = DeepNullable<Pick<GQLSettings, "auth">>;
export type FormProps = DeepNullable<
Pick<GQLSettings, "auth" | "accountFeatures">
>;
export type OnInitValuesFct = (values: DeepPartial<FormProps>) => void;
interface Props {
@@ -29,6 +34,7 @@ interface Props {
submitting?: boolean;
addSubmitHook: AddSubmitHook<FormProps>;
auth: AuthData;
settings: SettingsData;
}
class AuthConfigContainer extends React.Component<Props> {
@@ -96,16 +102,28 @@ class AuthConfigContainer extends React.Component<Props> {
public render() {
return (
<AuthConfig
disabled={this.props.submitting}
auth={this.props.auth}
onInitValues={this.handleOnInitValues}
/>
<HorizontalGutter size="double">
<AccountFeaturesConfigContainer
onInitValues={this.handleOnInitValues}
settings={this.props.settings}
disabled={this.props.submitting}
/>
<AuthConfig
disabled={this.props.submitting}
auth={this.props.auth}
onInitValues={this.handleOnInitValues}
/>
</HorizontalGutter>
);
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment AuthConfigContainer_settings on Settings {
...AccountFeaturesConfigContainer_settings
}
`,
auth: graphql`
fragment AuthConfigContainer_auth on Auth {
...FacebookConfigContainer_auth
@@ -25,6 +25,7 @@ class AuthConfigRoute extends React.Component<Props> {
}
return (
<AuthConfigContainer
settings={this.props.data.settings}
auth={this.props.data.settings.auth}
form={this.props.form}
submitting={this.props.submitting}
@@ -37,6 +38,7 @@ const enhanced = withRouteConfig<Props>({
query: graphql`
query AuthConfigRouteContainerQuery {
settings {
...AuthConfigContainer_settings
auth {
...AuthConfigContainer_auth
}
File diff suppressed because it is too large Load Diff
+5
View File
@@ -141,6 +141,11 @@ export const settings = createFixture<GQLSettings>({
},
},
},
accountFeatures: {
downloadComments: true,
changeUsername: true,
deleteAccount: true,
},
});
export const settingsWithEmptyAuth = createFixture<GQLSettings>(
@@ -168,7 +168,7 @@ const ChangeUsernameContainer: FunctionComponent<Props> = ({
{!showEditForm && (
<Flex alignItems="baseline">
<Typography variant="header2">{viewer.username}</Typography>
{canChangeLocalAuth && (
{canChangeLocalAuth && settings.accountFeatures.changeUsername && (
<Localized id="profile-changeUsername-edit">
<Button size="small" color="primary" onClick={toggleEditForm}>
edit
@@ -374,6 +374,9 @@ const enhanced = withFragmentContainer<Props>({
`,
settings: graphql`
fragment ChangeUsernameContainer_settings on Settings {
accountFeatures {
changeUsername
}
auth {
integrations {
local {
@@ -22,8 +22,12 @@ const SettingsContainer: FunctionComponent<Props> = ({ viewer, settings }) => (
<HorizontalGutter spacing={5} className={styles.root}>
<IgnoreUserSettingsContainer viewer={viewer} />
<ChangePasswordContainer settings={settings} />
<DownloadCommentsContainer viewer={viewer} />
<DeleteAccountContainer viewer={viewer} settings={settings} />
{settings.accountFeatures.downloadComments && (
<DownloadCommentsContainer viewer={viewer} />
)}
{settings.accountFeatures.deleteAccount && (
<DeleteAccountContainer viewer={viewer} settings={settings} />
)}
</HorizontalGutter>
);
@@ -37,6 +41,10 @@ const enhanced = withFragmentContainer<Props>({
`,
settings: graphql`
fragment SettingsContainer_settings on Settings {
accountFeatures {
downloadComments
deleteAccount
}
...ChangePasswordContainer_settings
...DeleteAccountContainer_settings
}
+5
View File
@@ -96,6 +96,11 @@ export const settings = createFixture<GQLSettings>({
charCount: {
enabled: false,
},
accountFeatures: {
downloadComments: true,
changeUsername: true,
deleteAccount: true,
},
});
export const settingsWithoutLocalAuth = createFixture<GQLSettings>(
@@ -1079,6 +1079,24 @@ type StoryConfiguration {
disableLazy: Boolean!
}
"""
CommenterAccountFeatures stores the configuration for commenter account features.
"""
type CommenterAccountFeatures {
"""
changeUsername when true, non-sso user may change username every 14 days
"""
changeUsername: Boolean!
"""
downloadComments when true, user may download their comment history
"""
downloadComments: Boolean!
"""
deleteAccount when true, non-sso user may permanently delete their account
"""
deleteAccount: Boolean!
}
"""
Settings stores the global settings for a given Tenant.
"""
@@ -1194,6 +1212,11 @@ type Settings {
"""
reaction: ReactionConfiguration!
"""
accountFeatures stores the configuration for commenter account features.
"""
accountFeatures: CommenterAccountFeatures!
"""
stories stores the configuration around stories.
"""
@@ -3108,6 +3131,24 @@ input ReactionConfigurationInput {
color: String
}
"""
CommenterAccountFeatures stores the configuration for commenter account features.
"""
input CommenterAccountFeaturesInput {
"""
changeUsername when true, non-sso user may change username every 14 days
"""
changeUsername: Boolean
"""
downloadComments when true, user may download their comment history
"""
downloadComments: Boolean
"""
deleteAccount when true, non-sso user may permanently delete their account
"""
deleteAccount: Boolean
}
"""
SettingsInput is the partial type of the Settings type for performing mutations.
"""
@@ -3206,6 +3247,11 @@ input SettingsInput {
reaction specifies the configuration for reactions.
"""
reaction: ReactionConfigurationInput
"""
accountFeatures specifies the configuration for accounts.
"""
accountFeatures: CommenterAccountFeaturesInput
}
"""
+14
View File
@@ -49,6 +49,15 @@ export interface AuthIntegrations {
facebook: FacebookAuthIntegration;
}
/**
* AccountFeatures are features enabled for commenter accounts
*/
export interface AccountFeatures {
changeUsername: boolean;
deleteAccount: boolean;
downloadComments: boolean;
}
/**
* Auth is the set of configured authentication integrations.
*/
@@ -112,4 +121,9 @@ export type Settings = GlobalModerationSettings &
* disableCommenting will disable commenting site-wide.
*/
disableCommenting: DisableCommenting;
/**
* AccountFeatures are features enabled for commenter accounts
*/
accountFeatures: AccountFeatures;
};
+5
View File
@@ -192,6 +192,11 @@ export async function createTenant(
},
disableLazy: false,
},
accountFeatures: {
changeUsername: false,
deleteAccount: false,
downloadComments: false,
},
createdAt: now,
};
+7
View File
@@ -612,6 +612,10 @@ function enabledAuthenticationIntegrations(
* @param user the User that we are updating
*/
function canUpdateLocalProfile(tenant: Tenant, user: User): boolean {
if (!tenant.accountFeatures.changeUsername) {
return false;
}
if (!hasLocalProfile(user)) {
return false;
}
@@ -965,6 +969,9 @@ export async function requestCommentsDownload(
user: User,
now: Date
) {
if (!tenant.accountFeatures.downloadComments) {
throw new Error("Downloading comments is not enabled");
}
// Check to see if the user is allowed to download this now.
if (
user.lastDownloadedAt &&
+16
View File
@@ -745,3 +745,19 @@ configure-general-reactions-sort-input =
.placehodlder = E.g. Most Respected
configure-general-reactions-preview = Preview
configure-general-reaction-sortMenu-sortBy = Sort by
configure-account-features-title = Commenter account mangement features
configure-account-features-explanation =
You can enable and disable certain features for your commenters to use
within their Profile. These features also assist towards GDPR
compliance.
configure-account-features-allow = Allow users to:
configure-account-features-change-usernames = Change their usernames
configure-account-features-change-usernames-details = Usernames can be changed once every 14 days.
configure-account-features-yes = Yes
configure-account-features-no = No
configure-account-features-download-comments = Download their comments
configure-account-features-download-comments-details = Commenters can download a csv of their comment history.
configure-account-features-delete-account = Delete their account
configure-account-features-delete-account-details =
Removes all of their comment data, username, and email address from the site and the database.