);
diff --git a/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
index a10369850..95184dffd 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/CommentTombstone.js
@@ -13,6 +13,8 @@ class CommentTombstone extends React.Component {
return t('framework.comment_is_ignored');
case 'reject':
return t('framework.comment_is_rejected');
+ case 'deleted':
+ return t('framework.comment_is_deleted');
default:
return t('framework.comment_is_hidden');
}
diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
index 0a7a1951c..17d3132ea 100644
--- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js
@@ -372,6 +372,7 @@ const slots = [
'streamTabsPrepend',
'streamTabPanes',
'streamFilter',
+ 'stream',
];
const fragments = {
diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js
index c5704f719..bf75fa1a8 100644
--- a/client/coral-framework/graphql/fragments.js
+++ b/client/coral-framework/graphql/fragments.js
@@ -26,6 +26,7 @@ export default {
'UpdateAssetSettingsResponse',
'UpdateAssetStatusResponse',
'UpdateSettingsResponse',
- 'ChangePasswordResponse'
+ 'ChangePasswordResponse',
+ 'UpdateEmailAddressResponse'
),
};
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
index 8dc8fe113..e30c12e06 100644
--- a/client/coral-framework/utils/index.js
+++ b/client/coral-framework/utils/index.js
@@ -1,6 +1,7 @@
import { gql } from 'react-apollo';
import t from 'coral-framework/services/i18n';
import union from 'lodash/union';
+import get from 'lodash/get';
import { capitalize } from 'coral-framework/helpers/strings';
import assignWith from 'lodash/assignWith';
import mapValues from 'lodash/mapValues';
@@ -221,6 +222,13 @@ export function isCommentActive(commentStatus) {
return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0;
}
+export function isCommentDeleted(comment) {
+ return (
+ get(comment, 'body', null) === null ||
+ get(comment, 'deleted_at', null) !== null
+ );
+}
+
export function getShallowChanges(a, b) {
return union(Object.keys(a), Object.keys(b)).filter(key => a[key] !== b[key]);
}
diff --git a/config.js b/config.js
index a99e08923..6a1f999a7 100644
--- a/config.js
+++ b/config.js
@@ -212,6 +212,13 @@ const CONFIG = {
RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC,
RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET,
+ // RECAPTCHA_WINDOW is the rate limit's time interval
+ RECAPTCHA_WINDOW: process.env.TALK_RECAPTCHA_WINDOW || '10m',
+
+ // After RECAPTCHA_INCORRECT_TRIGGER incorrect attempts, recaptcha will be required.
+ RECAPTCHA_INCORRECT_TRIGGER:
+ process.env.TALK_RECAPTCHA_INCORRECT_TRIGGER || 5,
+
// WEBSOCKET_LIVE_URI is the absolute url to the live endpoint.
WEBSOCKET_LIVE_URI: process.env.TALK_WEBSOCKET_LIVE_URI || null,
diff --git a/docs/_config.yml b/docs/_config.yml
index 6c0da733b..ac90dbc96 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -41,7 +41,7 @@ relative_link: false
future: true
highlight:
enable: false
-
+
# Home page setting
# path: Root path for your blogs index page. (default = '')
# per_page: Posts displayed per page. (0 = disable pagination)
@@ -114,6 +114,8 @@ sidebar:
url: /integrating/styling-css/
- title: Translations and i18n
url: /integrating/translations-i18n/
+ - title: GDPR Compliance
+ url: /integrating/gdpr/
- title: Product Guide
children:
- title: How Talk Works
diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md
index b2ad29ad0..5b638401d 100644
--- a/docs/source/02-02-advanced-configuration.md
+++ b/docs/source/02-02-advanced-configuration.md
@@ -316,6 +316,18 @@ default to providing only a time based lockout. Refer to
[reCAPTCHA](https://www.google.com/recaptcha/intro/index.html) for information
on getting an account setup.
+## TALK_RECAPTCHA_WINDOW
+
+The rate limit time interval that there can be [TALK_RECAPTCHA_INCORRECT_TRIGGER](#talk_recaptcha_incorrect_trigger) incorrect attempts until the reCAPTCHA is
+marked as required, parsed by
+[ms](https://www.npmjs.com/package/ms). (Default `10m`)
+
+## TALK_RECAPTCHA_INCORRECT_TRIGGER
+
+The number of times that an incorrect login can be entered before within a time
+perioud indicated by [TALK_RECAPTCHA_WINDOW](#talk_recaptcha_window) until the
+reCAPTCHA is marked as required. (Default `5`)
+
## TALK_REDIS_CLIENT_CONFIGURATION
Configuration overrides for the redis client configuration in a JSON encoded
@@ -531,4 +543,4 @@ Sets the logging level for the context logger (from [Bunyan](https://github.com/
A JSON string representing the configuration passed to the
[fetch](https://www.npmjs.com/package/node-fetch) call for the scraper. It
can be used to set an authorization header, or change the user agent. (Default
-`{}`)
\ No newline at end of file
+`{}`)
diff --git a/docs/source/03-08-gdpr.md b/docs/source/03-08-gdpr.md
new file mode 100644
index 000000000..9b1050454
--- /dev/null
+++ b/docs/source/03-08-gdpr.md
@@ -0,0 +1,53 @@
+---
+title: GDPR Compliance
+permalink: /integrating/gdpr/
+---
+
+In order to facilitate compliance with the
+[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
+can enable the following plugins:
+
+- [talk-plugin-auth](/talk/plugin/talk-plugin-auth) - to facilitate username and password changes
+- [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association
+- [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 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
+[Integrating Authentication](/talk/integrating/authentication/), we have also
+provided tools for those newsrooms to integrate GDPR features into their
+existing workflows.
+
+### Account Data
+
+Through the [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data)
+plugin we allow users to download and delete their account data easily. For
+custom integrations, this isn't always possible, so we instead provide some
+GraphQL mutations designed to allow you to integrate it into your existing user
+interfaces or exports.
+
+- `downloadUser(id: ID!)` - lets you grab the direct link to download a users
+ account in a zip format. From there, you can integrate it into your existing
+ data export or simply proxy it to the user to allow them to download it
+ elsewhere in your UI.
+- `delUser(id: ID!)` - lets you delete the specified user
+
+**Note: These mutations require an administrative token**
+
+If you would prefer to write your own user interfaces or integrate it into your
+own, you can disable the client plugin for [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data)
+but keep the server side plugin active (See [Server and Client Plugins](/talk/plugins/#server-and-client-plugins) for more information).
diff --git a/docs/source/_data/plugins.yml b/docs/source/_data/plugins.yml
index a5621b0d6..0e09de210 100644
--- a/docs/source/_data/plugins.yml
+++ b/docs/source/_data/plugins.yml
@@ -3,10 +3,17 @@
tags:
- moderation
- name: talk-plugin-auth
- description: Enables internal authentication from Coral.
+ description: Enables internal sign-in authentication.
tags:
- default
- auth
+ - gdpr
+- name: talk-plugin-local-auth
+ description: Enables email and password based authentication.
+ tags:
+ - default
+ - auth
+ - gdpr
- name: talk-plugin-author-menu
description: Enables the comment author name plugin area.
tags:
diff --git a/docs/source/integrating/authentication.md b/docs/source/integrating/authentication.md
index 2a78944a6..b7340caf5 100644
--- a/docs/source/integrating/authentication.md
+++ b/docs/source/integrating/authentication.md
@@ -74,7 +74,6 @@ example issuer and Talk must match:
|------|----------------------|
|`JWT_ISSUER`|`JWT_ISSUER`|
|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
-|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
|`SECRET`|`JWT_SECRET`*|
\* Note that secrets is a pretty complex topic, refer to the
@@ -83,4 +82,4 @@ reference, the basic takeaway is that the secret used to sign the tokens issued
by the issuer must be able to be verified by Talk.
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound)
-reference.
\ No newline at end of file
+reference.
diff --git a/docs/source/plugin/talk-plugin-local-auth.md b/docs/source/plugin/talk-plugin-local-auth.md
new file mode 120000
index 000000000..918f29118
--- /dev/null
+++ b/docs/source/plugin/talk-plugin-local-auth.md
@@ -0,0 +1 @@
+../../../plugins/talk-plugin-local-auth/README.md
\ No newline at end of file
diff --git a/docs/source/plugins/overview.md b/docs/source/plugins/overview.md
index 26230dc00..046f4db87 100644
--- a/docs/source/plugins/overview.md
+++ b/docs/source/plugins/overview.md
@@ -9,11 +9,16 @@ functionality. We provide methods to inject behavior into the server side and
the client side application to affect different parts of the application
life cycle.
-## Recipes
+## Server and Client Plugins
-Recipes are plugin templates provided by the Coral Core team. Developers can use
-these recipes to build their own plugins. You can find all the Talk recipes
-here: [github.com/coralproject/talk-recipes](https://github.com/coralproject/talk-recipes/).
+When you're adding a plugin to Talk, you can specify it in the `client` and/or
+the `server` section. If you only want to enable the server side component of a
+plugin, you simply only specify the plugin in the `server` section. If you only
+want the client side plugin, the `client` section.
+
+Plugins listed in the [Plugins Directory](/talk/plugins-directory/) will
+indicate if they have/support a client/server plugin, and should be activated
+accordingly.
## Plugin Registration
@@ -117,3 +122,9 @@ assets inside the image as well.
For more information on the onbuild image, refer to the
[Installation from Docker](/talk/installation-from-docker/) documentation.
+
+## Recipes
+
+Recipes are plugin templates provided by the Coral Core team. Developers can use
+these recipes to build their own plugins. You can find all the Talk recipes
+here: [github.com/coralproject/talk-recipes](https://github.com/coralproject/talk-recipes/).
diff --git a/docs/source/plugins/plugins-directory.md b/docs/source/plugins/plugins-directory.md
index ca9b26734..9161d0d68 100644
--- a/docs/source/plugins/plugins-directory.md
+++ b/docs/source/plugins/plugins-directory.md
@@ -3,8 +3,9 @@ title: Plugins Directory
permalink: /plugins-directory/
layout: plugins
data: plugins
+class: plugins
---
Talk provides a growing ecosystem of plugins that interact with our extensive
Server and Client API's. Below you can search for a plugin to use with Talk and
-discover what their requirements and configuration are.
\ No newline at end of file
+discover what their requirements and configuration are.
diff --git a/docs/themes/coral/layout/plugins.swig b/docs/themes/coral/layout/plugins.swig
index 959908105..adb60be30 100644
--- a/docs/themes/coral/layout/plugins.swig
+++ b/docs/themes/coral/layout/plugins.swig
@@ -4,9 +4,9 @@
{{ page.title }}
{% endif %}
-
+
{{ page.content }}
-
+
@@ -15,6 +15,7 @@
Enter a few keywords about the plugin to filter the list
+
{% for plugin in _.sortBy(site.data[page.data], 'name') %}
@@ -25,7 +26,7 @@
{% if plugin.tags %}
{% for tag in plugin.tags %}
- {{ tag }}
+ {{ tag }}
{% endfor %}
{% endif %}
@@ -35,4 +36,4 @@
{% endfor %}
-
\ No newline at end of file
+
diff --git a/docs/themes/coral/source/css/talk.scss b/docs/themes/coral/source/css/talk.scss
index 391337876..afbd6f3af 100644
--- a/docs/themes/coral/source/css/talk.scss
+++ b/docs/themes/coral/source/css/talk.scss
@@ -448,3 +448,17 @@ a.brand {
.plugin {
display: none;
}
+
+.badge-tag {
+ font-family: monospace;
+}
+
+.badge-tag-default {
+ background: #28a745;
+ color: #fff;
+}
+
+.badge-tag-gdpr {
+ background: rgb(0, 102, 176);
+ color: #fff;
+}
diff --git a/docs/themes/coral/source/js/plugins.js b/docs/themes/coral/source/js/plugins.js
index 83f14d356..26f7707c3 100644
--- a/docs/themes/coral/source/js/plugins.js
+++ b/docs/themes/coral/source/js/plugins.js
@@ -1,6 +1,17 @@
/* global lunr */
/* eslint-env browser */
+// Sourced from https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
+function getParameterByName(name, url) {
+ if (!url) url = window.location.href;
+ name = name.replace(/[\[\]]/g, '\\$&');
+ var regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)'),
+ results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+}
+
// Sourced from https://github.com/hexojs/site/blob/8e8ed4901769abbf76263125f82832df76ced58b/themes/navy/source/js/plugins.js.
(function() {
'use strict';
@@ -60,6 +71,12 @@
updateCount(elements.length);
}
+ var searchParam = getParameterByName('q');
+ if (searchParam && searchParam.length > 0) {
+ $input.value = searchParam;
+ search(searchParam);
+ }
+
$input.addEventListener('input', function() {
var value = this.value;
diff --git a/graph/mutators/user.js b/graph/mutators/user.js
index 04fdcd366..8657905d9 100644
--- a/graph/mutators/user.js
+++ b/graph/mutators/user.js
@@ -8,7 +8,7 @@ const {
SET_USER_BAN_STATUS,
SET_USER_SUSPENSION_STATUS,
UPDATE_USER_ROLES,
- DELETE_USER,
+ DELETE_OTHER_USER,
CHANGE_PASSWORD,
} = require('../../perms/constants');
@@ -93,7 +93,7 @@ const delUser = async (ctx, id) => {
updateBatchSize: 10000,
});
- // Remove all actions against comments.
+ // Remove all actions against this users comments.
await transformSingleWithCursor(
Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
actionDecrTransformer,
@@ -112,34 +112,50 @@ const delUser = async (ctx, id) => {
.setOptions({ multi: true })
.remove();
- // Removes all the user's reply counts on each of the comments that they
- // have commented on.
+ // Remove the user from all other user's ignore lists.
+ await User.update(
+ { ignoresUsers: user.id },
+ {
+ $pull: { ignoresUsers: user.id },
+ },
+ { multi: true }
+ );
+
+ // For each comment that the user has authored, purge the comment data from it
+ // and unset their id from those comments.
await transformSingleWithCursor(
- Comment.collection.aggregate([
- { $match: { author_id: user.id } },
- {
- $group: {
- _id: '$parent_id',
- count: { $sum: 1 },
- },
- },
- ]),
- ({ _id: parent_id, count }) => ({
- query: { id: parent_id },
- update: {
- $inc: {
- reply_count: -1 * count,
- },
+ Comment.collection.find({ author_id: user.id }),
+ ({
+ id,
+ asset_id,
+ status,
+ parent_id,
+ reply_count,
+ created_at,
+ updated_at,
+ }) => ({
+ query: { id },
+ replace: {
+ id,
+ body: null,
+ body_history: [],
+ asset_id,
+ author_id: null,
+ status_history: [],
+ status,
+ parent_id,
+ reply_count,
+ action_counts: {},
+ tags: [],
+ metadata: {},
+ deleted_at: new Date(),
+ created_at,
+ updated_at,
},
}),
Comment
);
- // Remove all the user's comments.
- await Comment.where({ author_id: user.id })
- .setOptions({ multi: true })
- .remove();
-
// Remove the user.
await user.remove();
};
@@ -225,7 +241,7 @@ module.exports = ctx => {
setUserSuspensionStatus(ctx, id, until, message);
}
- if (ctx.user.can(DELETE_USER)) {
+ if (ctx.user.can(DELETE_OTHER_USER)) {
mutators.User.del = id => delUser(ctx, id);
}
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index 064835f8f..e562c062c 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -4,6 +4,7 @@ const {
SEARCH_ACTIONS,
SEARCH_COMMENT_STATUS_HISTORY,
VIEW_BODY_HISTORY,
+ VIEW_COMMENT_DELETED_AT,
} = require('../../perms/constants');
const {
decorateWithTags,
@@ -23,7 +24,9 @@ const Comment = {
return Comments.get.load(parent_id);
},
user({ author_id }, _, { loaders: { Users } }) {
- return Users.getByID.load(author_id);
+ if (author_id) {
+ return Users.getByID.load(author_id);
+ }
},
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
// Don't bother looking up replies if there aren't any there!
@@ -83,6 +86,7 @@ decorateWithTags(Comment);
decorateWithPermissionCheck(Comment, {
actions: [SEARCH_ACTIONS],
status_history: [SEARCH_COMMENT_STATUS_HISTORY],
+ deleted_at: [VIEW_COMMENT_DELETED_AT],
});
// Protect privileged fields.
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 03b199e0e..b5c193e3d 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -503,7 +503,7 @@ type Comment {
id: ID!
# The actual comment data.
- body: String!
+ body: String
# The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or
# the author.
@@ -537,6 +537,9 @@ type Comment {
# The status history of the comment. Requires the `ADMIN` or `MODERATOR` role.
status_history: [CommentStatusHistory!]
+ # The date that the comment was deleted at if it was.
+ deleted_at: Date
+
# The time when the comment was created
created_at: Date!
diff --git a/locales/en.yml b/locales/en.yml
index 393d01e23..3d2fe88c7 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -250,7 +250,9 @@ en:
ALREADY_EXISTS: "Resource already exists"
INVALID_ASSET_URL: "Assert URL is invalid"
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
+ INCORRECT_PASSWORD: "Incorrect Password"
email: "Please enter a valid email."
+ DELETION_NOT_SCHEDULED: "Deletion was not scheduled"
confirm_password: "Passwords don't match. Please check again"
network_error: "Failed to connect to server. Check your internet connection and try again."
email_not_verified: "Email address {0} not verified."
@@ -271,6 +273,7 @@ en:
comment: comment
comment_is_ignored: "This comment is hidden because you ignored this user."
comment_is_rejected: "You have rejected this comment."
+ comment_is_deleted: "This comment was deleted."
comment_is_hidden: "This comment is not available."
comments: comments
configure_stream: "Configure"
diff --git a/locales/es.yml b/locales/es.yml
index 628dd8faa..41e337df8 100644
--- a/locales/es.yml
+++ b/locales/es.yml
@@ -243,6 +243,7 @@ es:
ALREADY_EXISTS: "Resource already exists"
INVALID_ASSET_URL: "La URL del articulo no es valida"
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
+ INCORRECT_PASSWORD: "Contraseña Incorrecta"
email: "No es un correo válido"
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo."
diff --git a/models/schema/comment.js b/models/schema/comment.js
index d9586e850..a211884ff 100644
--- a/models/schema/comment.js
+++ b/models/schema/comment.js
@@ -58,8 +58,6 @@ const Comment = new Schema(
},
body: {
type: String,
- required: [true, 'The body is required.'],
- minlength: 2,
},
body_history: [BodyHistoryItemSchema],
asset_id: String,
@@ -89,6 +87,12 @@ const Comment = new Schema(
// Tags are added by the self or by administrators.
tags: [TagLinkSchema],
+ // deleted_at stores the date that the given comment was deleted.
+ deleted_at: {
+ type: Date,
+ default: null,
+ },
+
// Additional metadata stored on the field.
metadata: {
default: {},
diff --git a/models/schema/index.js b/models/schema/index.js
index 976b437c0..c33402078 100644
--- a/models/schema/index.js
+++ b/models/schema/index.js
@@ -1,5 +1,3 @@
-const { CREATE_MONGO_INDEXES } = require('../../config');
-
const Action = require('./action');
const Asset = require('./asset');
const Comment = require('./comment');
@@ -7,15 +5,4 @@ const Migration = require('./migration');
const Setting = require('./setting');
const User = require('./user');
-const schema = { Action, Asset, Comment, Migration, Setting, User };
-
-// Provide the schema to each of the plugins so that they can add in indexes if
-// it is enabled.
-if (CREATE_MONGO_INDEXES) {
- const plugins = require('../../services/plugins');
- plugins.get('server', 'indexes').map(({ indexes }) => {
- indexes(schema);
- });
-}
-
-module.exports = schema;
+module.exports = { Action, Asset, Comment, Migration, Setting, User };
diff --git a/package.json b/package.json
index c7e9a78c1..1127dd05c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "talk",
- "version": "4.3.2",
+ "version": "4.4.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
@@ -18,7 +18,7 @@
"lint:js": "eslint bin/cli* .",
"lint": "npm-run-all lint:*",
"plugins:reconcile": "./bin/cli plugins reconcile",
- "test": "npm-run-all test:jest test:mocha",
+ "test": "npm-run-all test:jest test:server:mocha",
"test:jest": "NODE_ENV=test jest --runInBand",
"test:client": "NODE_ENV=test jest --projects client",
"test:server": "npm-run-all test:server:jest test:server:mocha",
@@ -79,11 +79,11 @@
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-react": "^6.23.0",
- "bunyan-debug-stream": "^1.0.8",
"bcryptjs": "^2.4.3",
"bowser": "^1.7.2",
"brotli-webpack-plugin": "^0.5.0",
"bunyan": "^1.8.12",
+ "bunyan-debug-stream": "^1.0.8",
"cli-table": "^0.3.1",
"clipboard": "^1.7.1",
"colors": "^1.1.2",
@@ -98,7 +98,6 @@
"dataloader": "^1.3.0",
"debug": "3.1.0",
"dialog-polyfill": "^0.4.9",
- "did-you-mean": "^0.0.1",
"dotenv": "^4.0.0",
"ejs": "^2.5.7",
"env-rewrite": "^1.0.2",
diff --git a/perms/constants/mutation.js b/perms/constants/mutation.js
index 57aa254e7..2d4ae04cb 100644
--- a/perms/constants/mutation.js
+++ b/perms/constants/mutation.js
@@ -18,6 +18,6 @@ module.exports = {
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
- DELETE_USER: 'DELETE_USER',
+ DELETE_OTHER_USER: 'DELETE_OTHER_USER',
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
};
diff --git a/perms/constants/query.js b/perms/constants/query.js
index 197c5d9f9..0c7f4024d 100644
--- a/perms/constants/query.js
+++ b/perms/constants/query.js
@@ -11,4 +11,5 @@ module.exports = {
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
VIEW_BODY_HISTORY: 'VIEW_BODY_HISTORY',
+ VIEW_COMMENT_DELETED_AT: 'VIEW_COMMENT_DELETED_AT',
};
diff --git a/perms/reducers/mutation.js b/perms/reducers/mutation.js
index 44f66cf88..8133fcd47 100644
--- a/perms/reducers/mutation.js
+++ b/perms/reducers/mutation.js
@@ -56,6 +56,7 @@ module.exports = (user, perm) => {
case types.UPDATE_USER_ROLES:
case types.CREATE_TOKEN:
case types.REVOKE_TOKEN:
+ case types.DELETE_OTHER_USER:
return check(user, ['ADMIN']);
default:
diff --git a/perms/reducers/query.js b/perms/reducers/query.js
index ed507139d..5c8b17307 100644
--- a/perms/reducers/query.js
+++ b/perms/reducers/query.js
@@ -14,6 +14,7 @@ module.exports = (user, perm) => {
case types.VIEW_USER_ROLE:
case types.VIEW_USER_EMAIL:
case types.VIEW_BODY_HISTORY:
+ case types.VIEW_COMMENT_DELETED_AT:
return check(user, ['ADMIN', 'MODERATOR']);
case types.LIST_OWN_TOKENS:
return check(user, ['ADMIN']);
diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js
index d7ca5fce9..d33e7acc9 100644
--- a/plugin-api/beta/client/hocs/index.js
+++ b/plugin-api/beta/client/hocs/index.js
@@ -27,5 +27,6 @@ export {
withSetCommentStatus,
withChangePassword,
withChangeUsername,
+ withUpdateEmailAddress,
} from 'coral-framework/graphql/mutations';
export { compose } from 'recompose';
diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js
index 3fe742569..aeb9841f9 100644
--- a/plugin-api/beta/client/utils/index.js
+++ b/plugin-api/beta/client/utils/index.js
@@ -8,4 +8,5 @@ export {
getErrorMessages,
getDefinitionName,
getShallowChanges,
+ createDefaultResponseFragments,
} from 'coral-framework/utils';
diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js
index da075d76c..5aa3063b3 100644
--- a/plugin-api/beta/server/getReactionConfig.js
+++ b/plugin-api/beta/server/getReactionConfig.js
@@ -2,23 +2,25 @@ const { SEARCH_OTHER_USERS } = require('../../../perms/constants');
const { ErrNotFound, ErrAlreadyExists } = require('../../../errors');
const pluralize = require('pluralize');
const sc = require('snake-case');
-// const { CREATE_MONGO_INDEXES } = require('../../../config');
+const { CREATE_MONGO_INDEXES } = require('../../../config');
+const Comment = require('models/comment');
function getReactionConfig(reaction) {
+ // Ensure that the reaction is a lowercase string.
reaction = reaction.toLowerCase();
- // if (CREATE_MONGO_INDEXES) {
- // // Create the index on the comment model based on the reaction config.
- // CommentModel.collection.createIndex(
- // {
- // created_at: 1,
- // [`action_counts.${sc(reaction)}`]: 1,
- // },
- // {
- // background: true,
- // }
- // );
- // }
+ if (CREATE_MONGO_INDEXES) {
+ // Create the index on the comment model based on the reaction config.
+ Comment.collection.createIndex(
+ {
+ created_at: 1,
+ [`action_counts.${sc(reaction)}`]: 1,
+ },
+ {
+ background: true,
+ }
+ );
+ }
const reactionPlural = pluralize(reaction);
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
@@ -127,17 +129,6 @@ function getReactionConfig(reaction) {
return {
typeDefs,
- indexes: ({ Comment }) => {
- Comment.index(
- {
- created_at: 1,
- [`action_counts.${sc(reaction)}`]: 1,
- },
- {
- background: true,
- }
- );
- },
context: {
Sort: () => ({
Comments: {
diff --git a/plugins.default.json b/plugins.default.json
index e1011aa87..0fbc6e658 100644
--- a/plugins.default.json
+++ b/plugins.default.json
@@ -1,9 +1,9 @@
{
"server": [
- "talk-plugin-auth",
"talk-plugin-featured-comments",
- "talk-plugin-respect",
- "talk-plugin-profile-data"
+ "talk-plugin-local-auth",
+ "talk-plugin-profile-data",
+ "talk-plugin-respect"
],
"client": [
"talk-plugin-auth",
@@ -11,15 +11,16 @@
"talk-plugin-featured-comments",
"talk-plugin-flag-details",
"talk-plugin-ignore-user",
+ "talk-plugin-local-auth",
"talk-plugin-member-since",
"talk-plugin-moderation-actions",
"talk-plugin-permalink",
+ "talk-plugin-profile-data",
"talk-plugin-respect",
"talk-plugin-sort-most-replied",
"talk-plugin-sort-most-respected",
"talk-plugin-sort-newest",
"talk-plugin-sort-oldest",
- "talk-plugin-viewing-options",
- "talk-plugin-profile-data"
+ "talk-plugin-viewing-options"
]
}
diff --git a/plugins/talk-plugin-auth/README.md b/plugins/talk-plugin-auth/README.md
index fd5218365..c09dc218b 100644
--- a/plugins/talk-plugin-auth/README.md
+++ b/plugins/talk-plugin-auth/README.md
@@ -14,3 +14,9 @@ utilize our internal authentication system.
To sync Talk auth with your own auth systems, you can use this plugin as a
template.
+
+## GDPR Compliance
+
+In order to facilitate compliance with the
+[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
+should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
diff --git a/plugins/talk-plugin-auth/client/index.js b/plugins/talk-plugin-auth/client/index.js
index 8fedb8033..13abce1d9 100644
--- a/plugins/talk-plugin-auth/client/index.js
+++ b/plugins/talk-plugin-auth/client/index.js
@@ -4,8 +4,6 @@ import SetUsernameDialog from './stream/containers/SetUsernameDialog';
import translations from './translations.yml';
import Login from './login/containers/Main';
import reducer from './login/reducer';
-import ChangePassword from './profile-settings/containers/ChangePassword';
-import ChangeUsername from './profile-settings/containers/ChangeUsername';
export default {
reducer,
@@ -13,7 +11,5 @@ export default {
slots: {
stream: [UserBox, SignInButton, SetUsernameDialog],
login: [Login],
- profileHeader: [ChangeUsername],
- profileSettings: [ChangePassword],
},
};
diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js
deleted file mode 100644
index 8188bdfbe..000000000
--- a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import React from 'react';
-import cn from 'classnames';
-import PropTypes from 'prop-types';
-import styles from './ChangeUsername.css';
-import { Button } from 'plugin-api/beta/client/components/ui';
-import ChangeUsernameDialog from './ChangeUsernameDialog';
-import { t } from 'plugin-api/beta/client/services';
-import InputField from './InputField';
-import { getErrorMessages } from 'coral-framework/utils';
-import { canUsernameBeUpdated } from 'coral-framework/utils/user';
-
-const initialState = {
- editing: false,
- showDialog: false,
- formData: {},
-};
-
-class ChangeUsername extends React.Component {
- state = initialState;
-
- clearForm = () => {
- this.setState(initialState);
- };
-
- enableEditing = () => {
- this.setState({
- editing: true,
- });
- };
-
- disableEditing = () => {
- this.setState({
- editing: false,
- });
- };
-
- cancel = () => {
- this.clearForm();
- this.disableEditing();
- };
-
- showDialog = () => {
- this.setState({
- showDialog: true,
- });
- };
-
- onSave = async () => {
- this.showDialog();
- };
-
- saveChanges = async () => {
- const { newUsername } = this.state.formData;
- const { changeUsername } = this.props;
-
- try {
- await changeUsername(newUsername);
- this.props.notify(
- 'success',
- t('talk-plugin-auth.change_username.changed_username_success_msg')
- );
- } catch (err) {
- this.props.notify('error', getErrorMessages(err));
- }
-
- this.clearForm();
- this.disableEditing();
- };
-
- onChange = e => {
- const { name, value } = e.target;
-
- this.setState(state => ({
- formData: {
- ...state.formData,
- [name]: value,
- },
- }));
- };
-
- closeDialog = () => {
- this.setState({
- showDialog: false,
- });
- };
-
- render() {
- const {
- username,
- emailAddress,
- root: { me: { state: { status } } },
- notify,
- } = this.props;
- const { editing, formData, showDialog } = this.state;
-
- return (
-
-
-
- {editing ? (
-
- )}
-
- );
- }
-}
-
-ChangeUsername.propTypes = {
- root: PropTypes.object.isRequired,
- changeUsername: PropTypes.func.isRequired,
- notify: PropTypes.func.isRequired,
- username: PropTypes.string,
- emailAddress: PropTypes.string,
-};
-
-export default ChangeUsername;
diff --git a/plugins/talk-plugin-auth/client/profile-settings/containers/ChangeUsername.js b/plugins/talk-plugin-auth/client/profile-settings/containers/ChangeUsername.js
deleted file mode 100644
index 87e1e18b5..000000000
--- a/plugins/talk-plugin-auth/client/profile-settings/containers/ChangeUsername.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { compose } from 'react-apollo';
-import { bindActionCreators } from 'redux';
-import { connect } from 'plugin-api/beta/client/hocs';
-import ChangeUsername from '../components/ChangeUsername';
-import { notify } from 'coral-framework/actions/notification';
-import { withChangeUsername } from 'plugin-api/beta/client/hocs';
-
-const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
-
-export default compose(connect(null, mapDispatchToProps), withChangeUsername)(
- ChangeUsername
-);
diff --git a/plugins/talk-plugin-auth/client/translations.yml b/plugins/talk-plugin-auth/client/translations.yml
index 7cc0d9267..aa7579d74 100644
--- a/plugins/talk-plugin-auth/client/translations.yml
+++ b/plugins/talk-plugin-auth/client/translations.yml
@@ -154,8 +154,20 @@ en:
bottom_note: "Note: You will not be able to change your username again for 14 days"
confirm_changes: "Confirm Changes"
username_does_not_match: "Username does not match"
- changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days."
+ cant_be_equal: "Your new {0} must be different to your current one"
change_username_attempt: "Username can't be updated. Usernames can be changed every 14 days"
+ change_email:
+ confirm_email_change: "Confirm Email Address Change"
+ description: "You are attempting to change your email address. Your new email address will be used for your login and to receive account notifications."
+ old_email: "Old Email Address"
+ new_email: "New Email Address"
+ enter_password: "Enter Password"
+ incorrect_password: "Incorrect Password"
+ confirm_change: "Confirm Change"
+ cancel: "Cancel"
+ change_email_msg: "Email Address Changed - Your email address has been successfully changed. This email address will now be used for signing in and email notifications."
+ changed_username_success_msg: "Username Changed - Your username has been successfully changed. You will not be able to change your user name for 14 days."
+ change_username_attempt: "Username can't be updated. Usernames can only be changed every 14 days."
de:
talk-plugin-auth:
login:
diff --git a/plugins/talk-plugin-facebook-auth/README.md b/plugins/talk-plugin-facebook-auth/README.md
index 19d003150..08780adda 100644
--- a/plugins/talk-plugin-facebook-auth/README.md
+++ b/plugins/talk-plugin-facebook-auth/README.md
@@ -27,4 +27,10 @@ Configuration:
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register)
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
- enabled.
\ No newline at end of file
+ enabled.
+
+## GDPR Compliance
+
+In order to facilitate compliance with the
+[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
+should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
diff --git a/plugins/talk-plugin-google-auth/README.md b/plugins/talk-plugin-google-auth/README.md
index f68b56c32..15eb5cef7 100644
--- a/plugins/talk-plugin-google-auth/README.md
+++ b/plugins/talk-plugin-google-auth/README.md
@@ -27,3 +27,9 @@ Configuration:
- `TALK_GOOGLE_CLIENT_SECRET` (**required**) - The Google OAuth2 client ID for
your Google login web app. You can learn more about getting a Google Client
ID at the [Google API Console](https://console.developers.google.com/apis/).
+
+## GDPR Compliance
+
+In order to facilitate compliance with the
+[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
+should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
diff --git a/plugins/talk-plugin-local-auth/README.md b/plugins/talk-plugin-local-auth/README.md
new file mode 100644
index 000000000..5fabe0d71
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/README.md
@@ -0,0 +1,26 @@
+---
+title: talk-plugin-local-auth
+permalink: /plugin/talk-plugin-local-auth/
+layout: plugin
+plugin:
+ name: talk-plugin-local-auth
+ default: true
+ provides:
+ - Client
+ - Server
+---
+
+This plugin will eventually contain all the local authentication code that is
+responsible for creating, resetting, and managing accounts provided locally
+through an email and password based login.
+
+## Features
+
+- *Email Change*: Allows users to change their existing email address on their account.
+- *Local Account Association*: Allows users that have signed up with an external auth strategy (such as Google) the ability to associate a email address and password for login. **Note: Existing users with external authentication will be prompted to setup a local account when they sign in and when new users create an account.**
+
+## GDPR Compliance
+
+In order to facilitate compliance with the
+[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
+should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines.
diff --git a/plugins/talk-plugin-local-auth/client/.eslintrc.json b/plugins/talk-plugin-local-auth/client/.eslintrc.json
new file mode 100644
index 000000000..c8a6db18a
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/client/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "@coralproject/eslint-config-talk/client"
+}
diff --git a/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.css b/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.css
new file mode 100644
index 000000000..a57c3662b
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.css
@@ -0,0 +1,77 @@
+.dialog {
+ border: none;
+ box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
+ width: 320px;
+ top: 10px;
+ font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
+ font-size: 14px;
+ border-radius: 4px;
+ padding: 12px 20px;
+}
+
+.close {
+ font-size: 20px;
+ line-height: 14px;
+ top: 10px;
+ right: 10px;
+ position: absolute;
+ display: block;
+ font-weight: bold;
+ color: #363636;
+ cursor: pointer;
+
+ &:hover {
+ color: #6b6b6b;
+ }
+}
+
+.title {
+ font-size: 1.3em;
+ margin-bottom: 8px;
+}
+
+.description {
+ font-size: 1em;
+ line-height: 20px;
+ margin: 0;
+}
+
+.item {
+ display: block;
+ color: #4C4C4D;
+ font-size: 1em;
+ margin-bottom: 2px;
+}
+
+.bottomActions {
+ text-align: right;
+}
+
+.emailChange {
+ margin: 18px 0;
+}
+
+.cancel {
+ border: 1px solid #787d80;
+ background-color: transparent;
+ height: 30px;
+ font-size: 0.9em;
+ line-height: normal;
+
+ &:hover {
+ background-color: #eaeaea;
+ }
+}
+
+.confirmChanges {
+ background-color: #3498DB;
+ border-color: #3498DB;
+ color: white;
+ height: 30px;
+ font-size: 0.9em;
+
+ &:hover {
+ background-color: #3ba3ec;
+ color: white;
+ }
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js b/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js
new file mode 100644
index 000000000..4bc15757e
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/client/components/ChangeEmailContentDialog.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './ChangeEmailContentDialog.css';
+import InputField from './InputField';
+import { Button } from 'plugin-api/beta/client/components/ui';
+import { t } from 'plugin-api/beta/client/services';
+
+class ChangeEmailContentDialog extends React.Component {
+ state = {
+ showError: false,
+ };
+
+ showError = () => {
+ this.setState({
+ showError: true,
+ });
+ };
+
+ confirmChanges = async () => {
+ await this.props.save();
+ this.props.next();
+ };
+
+ render() {
+ return (
+