mirror of
https://github.com/wassname/talk.git
synced 2026-07-20 12:40:47 +08:00
Merge branch 'master' of github.com:coralproject/talk into email-auth-gdpr
* 'master' of github.com:coralproject/talk: fix handle confirmed email better standardized styles, fixed mutation bug password patches fixed merge error Fix build fail Missed an s docs patch Add feature overview to GDPR docs InputField conflict moving mutations to /hocs folder moving mutations to /hocs folder withUpdateEmailAddress in the plugins hoc
This commit is contained in:
@@ -12,9 +12,19 @@ can enable the following plugins:
|
||||
- [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion
|
||||
|
||||
Even if you don't reside in a location where GDPR will apply, it is recommended
|
||||
to enable these features anyways to provide your users with control over their
|
||||
to enable these features as a best practice to provide your users with control over their
|
||||
own data.
|
||||
|
||||
## GPDR Feature Overview
|
||||
|
||||
Integrating our GDPR tools will give your users and organizations the following benefits:
|
||||
|
||||
- **Download my comment data**: Users can request a download of their comments. An email with a link is emailed to them to download a CSV with each comment they've made, what story it was made on, and the comment's ID and timestamp.
|
||||
- **Delete my acccount**: Users can request deletion of their account. Deleted account requests are pending for 24 hours to allow the user to download their comments, or to change their mind and reactivate their account before the expiry. Account deletions remove all of their comments from the site, all their comments and actions from the database, and their account info from our system.
|
||||
- **Add an email to an Oauth/external account**: Users are prompted to add an email to their non-Talk account (Facebook, Google, external, etc) so that they can take part in GDPR and other features requiring email communication.
|
||||
- **Change my username**: Users can update their username. This is capped at once every 2 weeks.
|
||||
- **Change my email**: Users can change their email.
|
||||
|
||||
## Custom Authentication Solutions
|
||||
|
||||
As many of the newsrooms who have integrated Talk have followed our guides on
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
.container {
|
||||
position: relative;
|
||||
color: #202020;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
border: solid 1px transparent;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
&.editing {
|
||||
padding: 10px;
|
||||
border-color: #979797;
|
||||
background-color: #EDEDED;
|
||||
|
||||
.actions {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -31,11 +36,11 @@
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
text-align: right;
|
||||
width: 280px;
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.detailLink {
|
||||
color: #00538A;
|
||||
color: #00538A;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
&:hover {
|
||||
@@ -59,7 +64,7 @@
|
||||
> i {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: #399ee2;
|
||||
color: white;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
.detailItemContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.columnDisplay {
|
||||
@@ -16,6 +17,10 @@
|
||||
}
|
||||
|
||||
.detailItemContent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.detailInput {
|
||||
border: solid 1px #787D80;
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
@@ -57,6 +62,7 @@
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.detailItemMessage {
|
||||
@@ -64,7 +70,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 6px;
|
||||
padding-top: 16px;
|
||||
|
||||
.warningIcon, .checkIcon {
|
||||
font-size: 17px;
|
||||
|
||||
@@ -30,41 +30,45 @@ const InputField = ({
|
||||
|
||||
return (
|
||||
<div className={styles.detailItem}>
|
||||
<div
|
||||
className={cn(styles.detailItemContainer, {
|
||||
[styles.columnDisplay]: columnDisplay,
|
||||
})}
|
||||
>
|
||||
<div className={cn(styles.detailItemContainer)}>
|
||||
{label && (
|
||||
<label className={styles.detailLabel} id={id}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
styles.detailItemContent,
|
||||
{ [styles.error]: hasError && showError },
|
||||
{ [styles.disabled]: disabled }
|
||||
)}
|
||||
className={cn(styles.detailItemContent, {
|
||||
[styles.columnDisplay]: columnDisplay,
|
||||
})}
|
||||
>
|
||||
{icon && <Icon name={icon} className={styles.detailIcon} />}
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
name={name}
|
||||
className={styles.detailValue}
|
||||
onChange={onChange}
|
||||
autoComplete="off"
|
||||
data-validation-type={validationType}
|
||||
disabled={disabled}
|
||||
{...inputValue}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailItemMessage}>
|
||||
{!hasError &&
|
||||
showSuccess &&
|
||||
value && <Icon className={styles.checkIcon} name="check_circle" />}
|
||||
{hasError && showError && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||
<div
|
||||
className={cn(
|
||||
styles.detailInput,
|
||||
{ [styles.error]: hasError && showError },
|
||||
{ [styles.disabled]: disabled }
|
||||
)}
|
||||
>
|
||||
{icon && <Icon name={icon} className={styles.detailIcon} />}
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
name={name}
|
||||
className={styles.detailValue}
|
||||
onChange={onChange}
|
||||
autoComplete="off"
|
||||
data-validation-type={validationType}
|
||||
disabled={disabled}
|
||||
{...inputValue}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailItemMessage}>
|
||||
{!hasError &&
|
||||
showSuccess &&
|
||||
value && (
|
||||
<Icon className={styles.checkIcon} name="check_circle" />
|
||||
)}
|
||||
{hasError && showError && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import update from 'immutability-helper';
|
||||
import get from 'lodash/get';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
UpdateEmailAddress: () => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Profile: previousData => {
|
||||
// Find the local profile (if they have one).
|
||||
const localIndex = findIndex(get(previousData, 'me.profiles', []), {
|
||||
provider: 'local',
|
||||
});
|
||||
if (localIndex < 0) {
|
||||
return previousData;
|
||||
}
|
||||
|
||||
// Mutate the confirmedAt, because we changed the email address, they
|
||||
// can't possibly be confirmed now as well.
|
||||
return update(previousData, {
|
||||
me: {
|
||||
profiles: {
|
||||
[localIndex]: {
|
||||
confirmedAt: {
|
||||
$set: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import ChangePassword from './containers/ChangePassword';
|
||||
import AddEmailAddressDialog from './containers/AddEmailAddressDialog';
|
||||
import Profile from './containers/Profile';
|
||||
import translations from './translations.yml';
|
||||
import graphql from './graphql';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
@@ -10,4 +11,5 @@ export default {
|
||||
profileSettings: [ChangePassword],
|
||||
stream: [AddEmailAddressDialog],
|
||||
},
|
||||
...graphql,
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ async function updateUserEmailAddress(ctx, email, confirmPassword) {
|
||||
},
|
||||
{
|
||||
$set: { 'profiles.$.id': email },
|
||||
$unset: { 'profiles.$.metadata.confirmed_at': 1 },
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.button {
|
||||
color: #787D80;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #787d80;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: #787D80;
|
||||
color: white;
|
||||
}
|
||||
|
||||
> i {
|
||||
font-size: 1.2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import moment from 'moment';
|
||||
import styles from './DeleteMyAccount.css';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import DeleteMyAccountDialog from './DeleteMyAccountDialog';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
@@ -59,28 +57,13 @@ class DeleteMyAccount extends React.Component {
|
||||
scheduledDeletionDate={scheduledDeletionDate}
|
||||
organizationContactEmail={organizationContactEmail}
|
||||
/>
|
||||
<h3
|
||||
className={cn(
|
||||
styles.title,
|
||||
'talk-plugin-auth--delete-my-account-description'
|
||||
)}
|
||||
>
|
||||
<h3 className="talk-plugin-auth--delete-my-account-description">
|
||||
{t('delete_request.delete_my_account')}
|
||||
</h3>
|
||||
<p
|
||||
className={cn(
|
||||
styles.description,
|
||||
'talk-plugin-auth--delete-my-account-description'
|
||||
)}
|
||||
>
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{t('delete_request.delete_my_account_description')}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
styles.description,
|
||||
'talk-plugin-auth--delete-my-account-description'
|
||||
)}
|
||||
>
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{scheduledDeletionDate &&
|
||||
t(
|
||||
'delete_request.already_submitted_request_description',
|
||||
@@ -88,18 +71,11 @@ class DeleteMyAccount extends React.Component {
|
||||
)}
|
||||
</p>
|
||||
{scheduledDeletionDate ? (
|
||||
<Button
|
||||
className={cn(styles.button, styles.secondary)}
|
||||
onClick={this.cancelAccountDeletion}
|
||||
>
|
||||
<Button onClick={this.cancelAccountDeletion}>
|
||||
{t('delete_request.cancel_account_deletion_request')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={cn(styles.button)}
|
||||
icon="delete"
|
||||
onClick={this.showDialog}
|
||||
>
|
||||
<Button icon="delete" onClick={this.showDialog}>
|
||||
{t('delete_request.delete_my_account')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -97,7 +97,7 @@ DeleteMyAccountDialog.propTypes = {
|
||||
showDialog: PropTypes.bool.isRequired,
|
||||
closeDialog: PropTypes.func.isRequired,
|
||||
requestAccountDeletion: PropTypes.func.isRequired,
|
||||
scheduledDeletionDate: PropTypes.any.isRequired,
|
||||
scheduledDeletionDate: PropTypes.any,
|
||||
organizationContactEmail: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import DownloadCommentHistory from '../components/DownloadCommentHistory';
|
||||
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { withRequestDownloadLink } from '../hocs';
|
||||
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
class DownloadCommentHistoryContainer extends Component {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
),
|
||||
},
|
||||
mutations: {
|
||||
RequestDownloadLink: () => ({
|
||||
DownloadCommentHistory: () => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Profile: previousData =>
|
||||
update(previousData, {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { withMutation } from 'plugin-api/beta/client/hocs';
|
||||
import { gql } from 'react-apollo';
|
||||
import update from 'immutability-helper';
|
||||
import moment from 'moment';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
export const withRequestDownloadLink = withMutation(
|
||||
gql`
|
||||
mutation RequestDownloadLink {
|
||||
mutation DownloadCommentHistory {
|
||||
requestDownloadLink {
|
||||
...RequestDownloadLinkResponse
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({ mutate }) => ({
|
||||
requestDownloadLink: () => mutate({}),
|
||||
requestDownloadLink: () => mutate({ variables: {} }),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
+4
-1
@@ -32,6 +32,7 @@ const i18n = require('./i18n');
|
||||
const Wordlist = require('./wordlist');
|
||||
const DomainList = require('./domain_list');
|
||||
const Limit = require('./limit');
|
||||
const { get } = require('lodash');
|
||||
|
||||
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
|
||||
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
|
||||
@@ -969,7 +970,9 @@ class Users {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (profile.metadata && profile.metadata.confirmed_at !== null) {
|
||||
// Check to see if the profile has already been confirmed.
|
||||
const confirmedAt = get(profile, 'metadata.confirmed_at', null);
|
||||
if (confirmedAt && confirmedAt < Date.now()) {
|
||||
throw new ErrEmailAlreadyVerified();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user