mirror of
https://github.com/wassname/talk.git
synced 2026-07-05 20:16:45 +08:00
Merge branch 'master' into user-detail-enh
This commit is contained in:
+2
-1
@@ -38,6 +38,7 @@ plugins/*
|
||||
!plugins/talk-plugin-google-auth
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-local-auth
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
@@ -53,6 +54,7 @@ plugins/*
|
||||
!plugins/talk-plugin-profile-data
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-rich-text
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-sort-most-downvoted
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
@@ -66,7 +68,6 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-upvote
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-rich-text
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const { head, map } = require('lodash');
|
||||
const Matcher = require('did-you-mean');
|
||||
|
||||
// We're requiring this here so it'll setup some promise rejection hooks to log
|
||||
// out.
|
||||
require('./util');
|
||||
|
||||
// Setup the program.
|
||||
program
|
||||
.command('serve', 'serve the application')
|
||||
.command('db', 'run database commands')
|
||||
@@ -24,20 +22,3 @@ program
|
||||
'provides utilities for interacting with the plugin system'
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
// If the command wasn't found, output help.
|
||||
const commands = map(program.commands, '_name');
|
||||
const command = head(program.args);
|
||||
if (!commands.includes(command)) {
|
||||
const m = new Matcher(commands);
|
||||
const similarCommands = m.list(command);
|
||||
|
||||
console.error(
|
||||
`cli '${command}' is not a talk cli command. See 'cli --help'.`
|
||||
);
|
||||
if (similarCommands.length > 0) {
|
||||
const sc = similarCommands.map(({ value }) => `\t${value}\n`).join('');
|
||||
console.error(`\nThe most similar commands are\n${sc}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+11
-8
@@ -4,7 +4,8 @@ const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingsService = require('../services/settings');
|
||||
const Settings = require('../services/settings');
|
||||
const cache = require('../services/cache');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -14,9 +15,12 @@ util.onshutdown([() => mongoose.disconnect()]);
|
||||
*/
|
||||
async function changeOrgName() {
|
||||
try {
|
||||
let settings = await SettingsService.retrieve();
|
||||
await cache.init();
|
||||
|
||||
let { organizationName } = await inquirer.prompt([
|
||||
// Get the original settings.
|
||||
const settings = await Settings.retrieve('organizationName');
|
||||
|
||||
const { organizationName } = await inquirer.prompt([
|
||||
{
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
@@ -25,9 +29,8 @@ async function changeOrgName() {
|
||||
]);
|
||||
|
||||
if (settings.organizationName !== organizationName) {
|
||||
settings.organizationName = organizationName;
|
||||
|
||||
await SettingsService.update(settings);
|
||||
// Set the organization name if there was a mutation to it.
|
||||
await Settings.update({ organizationName });
|
||||
|
||||
console.log('Settings were updated.');
|
||||
} else {
|
||||
@@ -36,9 +39,9 @@ async function changeOrgName() {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
} finally {
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+5
-2
@@ -4,7 +4,8 @@ require('../services/env');
|
||||
const debug = require('debug')('talk:util');
|
||||
const { uniq } = require('lodash');
|
||||
|
||||
const util = (module.exports = {});
|
||||
// Setup the utilities.
|
||||
const util = {};
|
||||
|
||||
/**
|
||||
* Stores an array of functions that should be executed in the event that the
|
||||
@@ -15,7 +16,7 @@ util.toshutdown = [];
|
||||
|
||||
/**
|
||||
* Calls all the shutdown functions and then ends the process.
|
||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
||||
* @param {Number} [defaultCode=0] default return code upon successful shutdown.
|
||||
*/
|
||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
if (signal) {
|
||||
@@ -63,3 +64,5 @@ process.on('unhandledRejection', err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.exports = util;
|
||||
|
||||
@@ -57,7 +57,7 @@ class OrganizationSettings extends React.Component {
|
||||
}
|
||||
|
||||
const updater = { organizationContactEmail: { $set: email } };
|
||||
const errorUpdater = { organizationEmail: { $set: error } };
|
||||
const errorUpdater = { organizationContactEmail: { $set: error } };
|
||||
|
||||
this.props.updatePending({ updater, errorUpdater });
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ ProfileContainer.propTypes = {
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
const slots = ['profileSections'];
|
||||
const slots = ['profileSections', 'profileSettings', 'profileHeader'];
|
||||
|
||||
const withProfileQuery = withQuery(
|
||||
gql`
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getActionSummary,
|
||||
iPerformedThisAction,
|
||||
isCommentActive,
|
||||
isCommentDeleted,
|
||||
getShallowChanges,
|
||||
} from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -744,8 +745,14 @@ export default class Comment extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={rootClassName} id={id}>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
{isCommentDeleted(comment) ? (
|
||||
<CommentTombstone action="deleted" />
|
||||
) : (
|
||||
<div>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
</div>
|
||||
)}
|
||||
{this.renderRepliesContainer()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -372,6 +372,7 @@ const slots = [
|
||||
'streamTabsPrepend',
|
||||
'streamTabPanes',
|
||||
'streamFilter',
|
||||
'stream',
|
||||
];
|
||||
|
||||
const fragments = {
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
'UpdateAssetSettingsResponse',
|
||||
'UpdateAssetStatusResponse',
|
||||
'UpdateSettingsResponse',
|
||||
'ChangePasswordResponse'
|
||||
'ChangePasswordResponse',
|
||||
'UpdateEmailAddressResponse'
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
`{}`)
|
||||
`{}`)
|
||||
|
||||
@@ -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).
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
reference.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../../plugins/talk-plugin-local-auth/README.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/).
|
||||
|
||||
@@ -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.
|
||||
discover what their requirements and configuration are.
|
||||
|
||||
+5
-4
@@ -4,9 +4,9 @@
|
||||
<h1>{{ page.title }}</h1>
|
||||
<hr/>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{{ page.content }}
|
||||
|
||||
|
||||
<div class="form-group mt-5">
|
||||
<div class="clearfix">
|
||||
<label for="exampleInputEmail1">Plugin Search</label>
|
||||
@@ -15,6 +15,7 @@
|
||||
<input type="text" class="form-control" id="plugin-search-input" aria-describedby="pluginSearchHelp">
|
||||
<small id="pluginSearchHelp" class="form-text text-muted">Enter a few keywords about the plugin to filter the list</small>
|
||||
</div>
|
||||
|
||||
<div class="row plugins">
|
||||
{% for plugin in _.sortBy(site.data[page.data], 'name') %}
|
||||
<div class="col-sm-6 plugin d-block">
|
||||
@@ -25,7 +26,7 @@
|
||||
{% if plugin.tags %}
|
||||
<p class="card-text">
|
||||
{% for tag in plugin.tags %}
|
||||
<span class="badge badge-{% if tag == "default" %}success{% else %}light{% endif %}">{{ tag }}</span>
|
||||
<a class="plain-link" title="Search for plugins with the {{ tag }} tag" href="?q={{ tag }}"><span class="badge badge-light badge-tag badge-tag-{{ _.kebabCase(tag) }}">{{ tag }}</span></a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -35,4 +36,4 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
<script>window.SEARCH_INDEX = {{ lunr_index(site.data[page.data]) }}</script>
|
||||
<script>window.SEARCH_INDEX = {{ lunr_index(site.data[page.data]) }}</script>
|
||||
|
||||
+14
@@ -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;
|
||||
}
|
||||
|
||||
+17
@@ -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;
|
||||
|
||||
|
||||
+41
-25
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
+1
-14
@@ -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 };
|
||||
|
||||
+3
-4
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -27,5 +27,6 @@ export {
|
||||
withSetCommentStatus,
|
||||
withChangePassword,
|
||||
withChangeUsername,
|
||||
withUpdateEmailAddress,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
export { compose } from 'recompose';
|
||||
|
||||
@@ -8,4 +8,5 @@ export {
|
||||
getErrorMessages,
|
||||
getDefinitionName,
|
||||
getShallowChanges,
|
||||
createDefaultResponseFragments,
|
||||
} from 'coral-framework/utils';
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<section
|
||||
className={cn('talk-plugin-auth--edit-profile', styles.container, {
|
||||
[styles.editing]: editing,
|
||||
})}
|
||||
>
|
||||
<ChangeUsernameDialog
|
||||
canUsernameBeUpdated={canUsernameBeUpdated(status)}
|
||||
showDialog={showDialog}
|
||||
onChange={this.onChange}
|
||||
formData={formData}
|
||||
username={username}
|
||||
closeDialog={this.closeDialog}
|
||||
saveChanges={this.saveChanges}
|
||||
notify={notify}
|
||||
/>
|
||||
|
||||
{editing ? (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.detailList}>
|
||||
<InputField
|
||||
icon="person"
|
||||
id="newUsername"
|
||||
name="newUsername"
|
||||
onChange={this.onChange}
|
||||
defaultValue={username}
|
||||
columnDisplay
|
||||
validationType="username"
|
||||
>
|
||||
<span className={styles.bottomText}>
|
||||
{t('talk-plugin-auth.change_username.change_username_note')}
|
||||
</span>
|
||||
</InputField>
|
||||
<InputField
|
||||
icon="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={emailAddress}
|
||||
validationType="username"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? (
|
||||
<p className={styles.email}>{emailAddress}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{editing ? (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.saveButton)}
|
||||
icon="save"
|
||||
onClick={this.onSave}
|
||||
disabled={
|
||||
!this.state.formData.newUsername ||
|
||||
this.state.formData.newUsername === username
|
||||
}
|
||||
>
|
||||
{t('talk-plugin-auth.change_username.save')}
|
||||
</Button>
|
||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
||||
{t('talk-plugin-auth.change_username.cancel')}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon="settings"
|
||||
onClick={this.enableEditing}
|
||||
>
|
||||
{t('talk-plugin-auth.change_username.edit_profile')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUsername.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
changeUsername: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ChangeUsername;
|
||||
@@ -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
|
||||
);
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<span className={styles.close} onClick={this.props.cancel}>
|
||||
×
|
||||
</span>
|
||||
<h1 className={styles.title}>
|
||||
{t('talk-plugin-local-auth.change_email.confirm_email_change')}
|
||||
</h1>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.description}>
|
||||
{t('talk-plugin-local-auth.change_email.description')}
|
||||
</p>
|
||||
<div className={styles.emailChange}>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-local-auth.change_email.old_email')}:{' '}
|
||||
{this.props.email}
|
||||
</span>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-local-auth.change_email.new_email')}:{' '}
|
||||
{this.props.formData.newEmail}
|
||||
</span>
|
||||
</div>
|
||||
<form>
|
||||
<InputField
|
||||
id="confirmPassword"
|
||||
label={t('talk-plugin-local-auth.change_email.enter_password')}
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
onChange={this.props.onChange}
|
||||
defaultValue=""
|
||||
hasError={
|
||||
!this.props.formData.confirmPassword && this.state.showError
|
||||
}
|
||||
errorMsg={t(
|
||||
'talk-plugin-local-auth.change_email.incorrect_password'
|
||||
)}
|
||||
showError={this.state.showError}
|
||||
columnDisplay
|
||||
showSuccess={false}
|
||||
/>
|
||||
</form>
|
||||
<div className={styles.bottomActions}>
|
||||
<Button className={styles.cancel} onClick={this.props.cancel}>
|
||||
{t('talk-plugin-local-auth.change_email.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.confirmChanges}
|
||||
onClick={this.confirmChanges}
|
||||
>
|
||||
{t('talk-plugin-local-auth.change_email.confirm_change')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeEmailContentDialog.propTypes = {
|
||||
save: PropTypes.func,
|
||||
next: PropTypes.func,
|
||||
cancel: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
formData: PropTypes.object,
|
||||
email: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ChangeEmailContentDialog;
|
||||
+12
-7
@@ -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;
|
||||
+18
-12
@@ -45,7 +45,9 @@ class ChangePassword extends React.Component {
|
||||
const cond = this.state.formData[field] === this.state.formData[field2];
|
||||
if (!cond) {
|
||||
this.addError({
|
||||
[field2]: t('talk-plugin-auth.change_password.passwords_dont_match'),
|
||||
[field2]: t(
|
||||
'talk-plugin-local-auth.change_password.passwords_dont_match'
|
||||
),
|
||||
});
|
||||
} else {
|
||||
this.removeError(field2);
|
||||
@@ -56,7 +58,7 @@ class ChangePassword extends React.Component {
|
||||
fieldValidation = (value, type, name) => {
|
||||
if (!value.length) {
|
||||
this.addError({
|
||||
[name]: t('talk-plugin-auth.change_password.required_field'),
|
||||
[name]: t('talk-plugin-local-auth.change_password.required_field'),
|
||||
});
|
||||
} else if (!validate[type](value)) {
|
||||
this.addError({ [name]: errorMsj[type] });
|
||||
@@ -113,7 +115,7 @@ class ChangePassword extends React.Component {
|
||||
});
|
||||
this.props.notify(
|
||||
'success',
|
||||
t('talk-plugin-auth.change_password.changed_password_msg')
|
||||
t('talk-plugin-local-auth.change_password.changed_password_msg')
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
@@ -139,15 +141,19 @@ class ChangePassword extends React.Component {
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn('talk-plugin-auth--change-password', styles.container, {
|
||||
[styles.editing]: editing,
|
||||
})}
|
||||
className={cn(
|
||||
'talk-plugin-local-auth--change-password',
|
||||
styles.container,
|
||||
{
|
||||
[styles.editing]: editing,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<h3 className={styles.title}>
|
||||
{t('talk-plugin-auth.change_password.change_password')}
|
||||
{t('talk-plugin-local-auth.change_password.change_password')}
|
||||
</h3>
|
||||
{editing && (
|
||||
<form className="talk-plugin-auth--change-password-form">
|
||||
<form className="talk-plugin-local-auth--change-password-form">
|
||||
<InputField
|
||||
id="oldPassword"
|
||||
label="Old Password"
|
||||
@@ -161,7 +167,7 @@ class ChangePassword extends React.Component {
|
||||
>
|
||||
<span className={styles.detailBottomBox}>
|
||||
<a className={styles.detailLink}>
|
||||
{t('talk-plugin-auth.change_password.forgot_password')}
|
||||
{t('talk-plugin-local-auth.change_password.forgot_password')}
|
||||
</a>
|
||||
</span>
|
||||
</InputField>
|
||||
@@ -197,16 +203,16 @@ class ChangePassword extends React.Component {
|
||||
onClick={this.onSave}
|
||||
disabled={this.isSubmitBlocked()}
|
||||
>
|
||||
{t('talk-plugin-auth.change_password.save')}
|
||||
{t('talk-plugin-local-auth.change_password.save')}
|
||||
</Button>
|
||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
||||
{t('talk-plugin-auth.change_password.cancel')}
|
||||
{t('talk-plugin-local-auth.change_password.cancel')}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<Button className={styles.button} onClick={this.enableEditing}>
|
||||
{t('talk-plugin-auth.change_password.edit')}
|
||||
{t('talk-plugin-local-auth.change_password.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -0,0 +1,73 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.bottomNote {
|
||||
font-size: 0.9em;
|
||||
line-height: 20px;
|
||||
padding-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bottomActions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.usernamesChange {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ChangeUsernameContentDialog.css';
|
||||
import InputField from './InputField';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
class ChangeUsernameContentDialog extends React.Component {
|
||||
state = {
|
||||
showError: false,
|
||||
};
|
||||
|
||||
showError = () => {
|
||||
this.setState({
|
||||
showError: true,
|
||||
});
|
||||
};
|
||||
|
||||
confirmChanges = async () => {
|
||||
if (this.formHasError()) {
|
||||
this.showError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.props.canUsernameBeUpdated) {
|
||||
this.props.notify(
|
||||
'error',
|
||||
t('talk-plugin-local-auth.change_username.change_username_attempt')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.props.save();
|
||||
this.props.next();
|
||||
};
|
||||
|
||||
formHasError = () =>
|
||||
this.props.formData.confirmNewUsername !== this.props.formData.newUsername;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<span className={styles.close} onClick={this.props.cancel}>
|
||||
×
|
||||
</span>
|
||||
<h1 className={styles.title}>
|
||||
{t('talk-plugin-local-auth.change_username.confirm_username_change')}
|
||||
</h1>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.description}>
|
||||
{t('talk-plugin-local-auth.change_username.description')}
|
||||
</p>
|
||||
<div className={styles.usernamesChange}>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-local-auth.change_username.old_username')}:{' '}
|
||||
{this.props.username}
|
||||
</span>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-local-auth.change_username.new_username')}:{' '}
|
||||
{this.props.formData.newUsername}
|
||||
</span>
|
||||
</div>
|
||||
<form>
|
||||
<InputField
|
||||
id="confirmNewUsername"
|
||||
label="Re-enter new username"
|
||||
name="confirmNewUsername"
|
||||
type="text"
|
||||
onChange={this.props.onChange}
|
||||
defaultValue=""
|
||||
hasError={this.formHasError() && this.state.showError}
|
||||
errorMsg={t(
|
||||
'talk-plugin-local-auth.change_username.username_does_not_match'
|
||||
)}
|
||||
showError={this.state.showError}
|
||||
columnDisplay
|
||||
showSuccess={false}
|
||||
validationType="username"
|
||||
>
|
||||
<span className={styles.bottomNote}>
|
||||
{t('talk-plugin-local-auth.change_username.bottom_note')}
|
||||
</span>
|
||||
</InputField>
|
||||
</form>
|
||||
<div className={styles.bottomActions}>
|
||||
<Button className={styles.cancel} onClick={this.props.cancel}>
|
||||
{t('talk-plugin-local-auth.change_username.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.confirmChanges}
|
||||
onClick={this.confirmChanges}
|
||||
>
|
||||
{t('talk-plugin-local-auth.change_username.confirm_changes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUsernameContentDialog.propTypes = {
|
||||
save: PropTypes.func,
|
||||
next: PropTypes.func,
|
||||
cancel: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
formData: PropTypes.object,
|
||||
username: PropTypes.string,
|
||||
canUsernameBeUpdated: PropTypes.bool.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ChangeUsernameContentDialog;
|
||||
+13
-10
@@ -26,7 +26,7 @@ class ChangeUsernameDialog extends React.Component {
|
||||
if (!this.props.canUsernameBeUpdated) {
|
||||
this.props.notify(
|
||||
'error',
|
||||
t('talk-plugin-auth.change_username.change_username_attempt')
|
||||
t('talk-plugin-local-auth.change_username.change_username_attempt')
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -42,25 +42,28 @@ class ChangeUsernameDialog extends React.Component {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.props.showDialog}
|
||||
className={cn(styles.dialog, 'talk-plugin-auth--edit-profile-dialog')}
|
||||
className={cn(
|
||||
styles.dialog,
|
||||
'talk-plugin-local-auth--edit-profile-dialog'
|
||||
)}
|
||||
>
|
||||
<span className={styles.close} onClick={this.props.closeDialog}>
|
||||
×
|
||||
</span>
|
||||
<h1 className={styles.title}>
|
||||
{t('talk-plugin-auth.change_username.confirm_username_change')}
|
||||
{t('talk-plugin-local-auth.change_username.confirm_username_change')}
|
||||
</h1>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.description}>
|
||||
{t('talk-plugin-auth.change_username.description')}
|
||||
{t('talk-plugin-local-auth.change_username.description')}
|
||||
</p>
|
||||
<div className={styles.usernamesChange}>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-auth.change_username.old_username')}:{' '}
|
||||
{t('talk-plugin-local-auth.change_username.old_username')}:{' '}
|
||||
{this.props.username}
|
||||
</span>
|
||||
<span className={styles.item}>
|
||||
{t('talk-plugin-auth.change_username.new_username')}:{' '}
|
||||
{t('talk-plugin-local-auth.change_username.new_username')}:{' '}
|
||||
{this.props.formData.newUsername}
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,7 +77,7 @@ class ChangeUsernameDialog extends React.Component {
|
||||
defaultValue=""
|
||||
hasError={this.formHasError() && this.state.showError}
|
||||
errorMsg={t(
|
||||
'talk-plugin-auth.change_username.username_does_not_match'
|
||||
'talk-plugin-local-auth.change_username.username_does_not_match'
|
||||
)}
|
||||
showError={this.state.showError}
|
||||
columnDisplay
|
||||
@@ -82,19 +85,19 @@ class ChangeUsernameDialog extends React.Component {
|
||||
validationType="username"
|
||||
>
|
||||
<span className={styles.bottomNote}>
|
||||
{t('talk-plugin-auth.change_username.bottom_note')}
|
||||
{t('talk-plugin-local-auth.change_username.bottom_note')}
|
||||
</span>
|
||||
</InputField>
|
||||
</form>
|
||||
<div className={styles.bottomActions}>
|
||||
<Button className={styles.cancel}>
|
||||
{t('talk-plugin-auth.change_username.cancel')}
|
||||
{t('talk-plugin-local-auth.change_username.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.confirmChanges}
|
||||
onClick={this.confirmChanges}
|
||||
>
|
||||
{t('talk-plugin-auth.change_username.confirm_changes')}
|
||||
{t('talk-plugin-local-auth.change_username.confirm_changes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ConfirmChangesDialog.css';
|
||||
import { Dialog } from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const initialState = { step: 0 };
|
||||
|
||||
class ConfirmChangesDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
goToNextStep = () => {
|
||||
this.setState(({ step }) => ({
|
||||
step: step + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.setState(initialState);
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.clear();
|
||||
this.props.closeDialog();
|
||||
};
|
||||
|
||||
continue = () => {
|
||||
this.goToNextStep();
|
||||
};
|
||||
|
||||
finish = () => {
|
||||
this.clear();
|
||||
this.props.closeDialog();
|
||||
this.props.finish();
|
||||
};
|
||||
|
||||
renderSteps = () => {
|
||||
const steps = React.Children.toArray(this.props.children)
|
||||
.filter(child => child.props.enable)
|
||||
.filter((_, i) => i === this.state.step);
|
||||
|
||||
return steps.map(child => {
|
||||
return React.cloneElement(child, {
|
||||
goToNextStep: this.goToNextStep,
|
||||
clear: this.clear,
|
||||
cancel: this.cancel,
|
||||
next:
|
||||
this.state.step === steps.length - 1 ? this.finish : this.continue,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.props.showDialog}
|
||||
className={cn(
|
||||
styles.dialog,
|
||||
'talk-plugin-local-auth--edit-profile-dialog'
|
||||
)}
|
||||
>
|
||||
{this.renderSteps()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmChangesDialog.propTypes = {
|
||||
children: PropTypes.node,
|
||||
closeDialog: PropTypes.func,
|
||||
showDialog: PropTypes.bool,
|
||||
finish: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ConfirmChangesDialog;
|
||||
+5
-1
@@ -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;
|
||||
@@ -64,7 +69,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 6px;
|
||||
padding-top: 16px;
|
||||
|
||||
.warningIcon, .checkIcon {
|
||||
font-size: 17px;
|
||||
+32
-28
@@ -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 },
|
||||
{ [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 },
|
||||
{ [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,274 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Profile.css';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import InputField from './InputField';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import ConfirmChangesDialog from './ConfirmChangesDialog';
|
||||
import ChangeUsernameContentDialog from './ChangeUsernameContentDialog';
|
||||
import ChangeEmailContentDialog from './ChangeEmailContentDialog';
|
||||
import { canUsernameBeUpdated } from 'coral-framework/utils/user';
|
||||
|
||||
const initialState = {
|
||||
editing: false,
|
||||
showDialog: false,
|
||||
formData: {},
|
||||
errors: {},
|
||||
};
|
||||
|
||||
class Profile 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();
|
||||
};
|
||||
|
||||
addError = err => {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: { ...errors, ...err },
|
||||
}));
|
||||
};
|
||||
|
||||
removeError = errKey => {
|
||||
this.setState(state => {
|
||||
const { [errKey]: _, ...errors } = state.errors;
|
||||
return {
|
||||
errors,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
fieldValidation = (value, type, name) => {
|
||||
if (!value.length) {
|
||||
this.addError({
|
||||
[name]: t('talk-plugin-local-auth.change_password.required_field'),
|
||||
});
|
||||
} else if (!validate[type](value)) {
|
||||
this.addError({ [name]: errorMsj[type] });
|
||||
} else {
|
||||
this.removeError(name);
|
||||
}
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
const { name, value, type, dataset } = e.target;
|
||||
const validationType = dataset.validationType || type;
|
||||
|
||||
this.setState(
|
||||
state => ({
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this.fieldValidation(value, validationType, name);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
closeDialog = () => {
|
||||
this.setState({
|
||||
showDialog: false,
|
||||
});
|
||||
};
|
||||
|
||||
hasError = err => {
|
||||
return Object.keys(this.state.errors).indexOf(err) !== -1;
|
||||
};
|
||||
|
||||
isSaveEnabled = () => {
|
||||
const { formData } = this.state;
|
||||
const { emailAddress, username } = this.props;
|
||||
const formHasErrors = !!Object.keys(this.state.errors).length;
|
||||
const validUsername =
|
||||
formData.newUsername && formData.newUsername !== username;
|
||||
const validEmail = formData.newEmail && formData.newEmail !== emailAddress;
|
||||
|
||||
return !formHasErrors && (validUsername || validEmail);
|
||||
};
|
||||
|
||||
saveUsername = async () => {
|
||||
const { newUsername } = this.state.formData;
|
||||
const { changeUsername } = this.props;
|
||||
|
||||
try {
|
||||
await changeUsername(this.props.root.me.id, newUsername);
|
||||
this.props.notify(
|
||||
'success',
|
||||
t('talk-plugin-local-auth.change_username.changed_username_success_msg')
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
saveEmail = async () => {
|
||||
const { newEmail, confirmPassword } = this.state.formData;
|
||||
|
||||
try {
|
||||
await this.props.updateEmailAddress({
|
||||
email: newEmail,
|
||||
confirmPassword,
|
||||
});
|
||||
this.props.notify(
|
||||
'success',
|
||||
t('talk-plugin-local-auth.change_email.change_email_msg')
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
finish = () => {
|
||||
this.clearForm();
|
||||
this.disableEditing();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
root: { me: { username, email, state: { status } } },
|
||||
notify,
|
||||
} = this.props;
|
||||
const { editing, formData, showDialog } = this.state;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'talk-plugin-local-auth--edit-profile',
|
||||
styles.container,
|
||||
{
|
||||
[styles.editing]: editing,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<ConfirmChangesDialog
|
||||
showDialog={showDialog}
|
||||
closeDialog={this.closeDialog}
|
||||
finish={this.finish}
|
||||
>
|
||||
<ChangeUsernameContentDialog
|
||||
notify={notify}
|
||||
canUsernameBeUpdated={canUsernameBeUpdated(status)}
|
||||
save={this.saveUsername}
|
||||
onChange={this.onChange}
|
||||
formData={this.state.formData}
|
||||
username={username}
|
||||
enable={formData.newUsername && username !== formData.newUsername}
|
||||
/>
|
||||
<ChangeEmailContentDialog
|
||||
save={this.saveEmail}
|
||||
onChange={this.onChange}
|
||||
formData={this.state.formData}
|
||||
email={email}
|
||||
enable={formData.newEmail && email !== formData.newEmail}
|
||||
/>
|
||||
</ConfirmChangesDialog>
|
||||
|
||||
{editing ? (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.detailList}>
|
||||
<InputField
|
||||
icon="person"
|
||||
id="newUsername"
|
||||
name="newUsername"
|
||||
onChange={this.onChange}
|
||||
defaultValue={username}
|
||||
validationType="username"
|
||||
columnDisplay
|
||||
>
|
||||
<span className={styles.bottomText}>
|
||||
{t(
|
||||
'talk-plugin-local-auth.change_username.change_username_note'
|
||||
)}
|
||||
</span>
|
||||
</InputField>
|
||||
<InputField
|
||||
icon="email"
|
||||
id="newEmail"
|
||||
name="newEmail"
|
||||
onChange={this.onChange}
|
||||
defaultValue={email}
|
||||
validationType="email"
|
||||
columnDisplay
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{email ? <p className={styles.email}>{email}</p> : null}
|
||||
</div>
|
||||
)}
|
||||
{editing ? (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.saveButton)}
|
||||
icon="save"
|
||||
onClick={this.onSave}
|
||||
disabled={!this.isSaveEnabled()}
|
||||
>
|
||||
{t('talk-plugin-local-auth.change_username.save')}
|
||||
</Button>
|
||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
||||
{t('talk-plugin-local-auth.change_username.cancel')}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon="settings"
|
||||
onClick={this.enableEditing}
|
||||
>
|
||||
{t('talk-plugin-local-auth.change_username.edit_profile')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Profile.propTypes = {
|
||||
updateEmailAddress: PropTypes.func.isRequired,
|
||||
changeUsername: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
changeUsername: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import Profile from '../components/Profile';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { withChangeUsername } from 'plugin-api/beta/client/hocs';
|
||||
import { withUpdateEmailAddress } from '../hocs';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
const withData = withFragments({
|
||||
root: gql`
|
||||
fragment TalkPluginLocalAuth_Profile_root on RootQuery {
|
||||
me {
|
||||
id
|
||||
email
|
||||
username
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
history {
|
||||
status
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withChangeUsername,
|
||||
withUpdateEmailAddress,
|
||||
withData
|
||||
)(Profile);
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import update from 'immutability-helper';
|
||||
import withMutation from 'coral-framework/hocs/withMutation';
|
||||
|
||||
export const withUpdateEmailAddress = withMutation(
|
||||
gql`
|
||||
mutation UpdateEmailAddress($input: UpdateEmailAddressInput!) {
|
||||
updateEmailAddress(input: $input) {
|
||||
...UpdateEmailAddressResponse
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({ mutate }) => ({
|
||||
updateEmailAddress: input => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
update: proxy => {
|
||||
const UpdateEmailAddressQuery = gql`
|
||||
query Talk_UpdateEmailAddress {
|
||||
me {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const prev = proxy.readQuery({ query: UpdateEmailAddressQuery });
|
||||
|
||||
const data = update(prev, {
|
||||
me: {
|
||||
email: { $set: input.email },
|
||||
},
|
||||
});
|
||||
|
||||
proxy.writeQuery({
|
||||
query: UpdateEmailAddressQuery,
|
||||
data,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import ChangePassword from './containers/ChangePassword';
|
||||
import Profile from './containers/Profile';
|
||||
import translations from './translations.yml';
|
||||
import graphql from './graphql';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
profileHeader: [Profile],
|
||||
profileSettings: [ChangePassword],
|
||||
},
|
||||
...graphql,
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
en:
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "Change Password"
|
||||
passwords_dont_match: "Passwords don`t match"
|
||||
required_field: "This field is required"
|
||||
forgot_password: "Forgot your password?"
|
||||
save: "Save"
|
||||
cancel: "Cancel"
|
||||
edit: "Edit"
|
||||
changed_password_msg: "Changed Password - Your password has been successfully changed"
|
||||
change_username:
|
||||
change_username_note: "Usernames can be changed every 14 days"
|
||||
save: "Save"
|
||||
edit_profile: "Edit Profile"
|
||||
cancel: "Cancel"
|
||||
confirm_username_change: "Confirm Username Change"
|
||||
description: "You are attempting to change your username. Your new username will appear on all of your past and future comments."
|
||||
old_username: "Old Username"
|
||||
new_username: "New Username"
|
||||
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"
|
||||
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."
|
||||
es:
|
||||
talk-plugin-local-auth:
|
||||
change_password:
|
||||
change_password: "Cambiar Contraseña"
|
||||
passwords_dont_match: "Las contraseñas no coinciden"
|
||||
required_field: "Este campo es requerido"
|
||||
forgot_password: "Olvidaste tu contraseña?"
|
||||
save: "Guardar"
|
||||
cancel: "Cancelar"
|
||||
edit: "Editar"
|
||||
changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
|
||||
change_username:
|
||||
change_username_note: "El usuario puede ser cambiado cada 14 días"
|
||||
save: "Guardar"
|
||||
edit_profile: "Editar Perfil"
|
||||
cancel: "Cancelar"
|
||||
confirm_username_change: "Confirmar Cambio de Usuario"
|
||||
description: "Estás intentando cambiar tu usuario. Tu nuevo usuario aparecerá en todos tus pasados y futuros comentarios."
|
||||
old_username: "Usuario viejo"
|
||||
new_username: "Usuario nuevo"
|
||||
bottom_note: "Nota: No podrás cambiar tu usuario por 14 días"
|
||||
confirm_changes: "Confirmar Cambios"
|
||||
username_does_not_match: "El usuario no coincide"
|
||||
changed_username_success_msg: "Usuario Actualizado - Tu usuario ha sido exitosamente actualizado. No podrás cambiar el usuario por 14 días."
|
||||
change_username_attempt: "El usuario no puede ser actualizado. Los usuarios pueden ser cambiados cada 14 días."
|
||||
@@ -0,0 +1,11 @@
|
||||
const typeDefs = require('./server/typeDefs');
|
||||
const resolvers = require('./server/resolvers');
|
||||
const mutators = require('./server/mutators');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
translations: path.join(__dirname, 'server', 'translations.yml'),
|
||||
typeDefs,
|
||||
mutators,
|
||||
resolvers,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
const { TalkError } = require('errors');
|
||||
|
||||
// ErrNoLocalProfile is returned when there is no existing local profile
|
||||
// attached to a user.
|
||||
class ErrNoLocalProfile extends TalkError {
|
||||
constructor() {
|
||||
super('No local profile associated with account', {
|
||||
translation_key: 'NO_LOCAL_PROFILE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrLocalProfile is returned when a profile is already attached to a user and
|
||||
// the user is trying to attach a new profile to it.
|
||||
class ErrLocalProfile extends TalkError {
|
||||
constructor() {
|
||||
super('Local profile already associated with account', {
|
||||
translation_key: 'LOCAL_PROFILE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrIncorrectPassword is returned when the password passed was incorrect.
|
||||
class ErrIncorrectPassword extends TalkError {
|
||||
constructor() {
|
||||
super('Password was incorrect', {
|
||||
translation_key: 'INCORRECT_PASSWORD',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ErrLocalProfile, ErrNoLocalProfile, ErrIncorrectPassword };
|
||||
@@ -0,0 +1,156 @@
|
||||
const { ErrNotAuthorized, ErrNotFound, ErrEmailTaken } = require('errors');
|
||||
const {
|
||||
ErrNoLocalProfile,
|
||||
ErrLocalProfile,
|
||||
ErrIncorrectPassword,
|
||||
} = require('./errors');
|
||||
const { get } = require('lodash');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
// hasLocalProfile checks a user's profiles to see if they already have a local
|
||||
// profile associated with their account.
|
||||
const hasLocalProfile = user =>
|
||||
get(user, 'profiles', []).some(({ provider }) => provider === 'local');
|
||||
|
||||
// updateUserEmailAddress will verify that the user has sent the correct
|
||||
// password followed by executing the email change and notifying the emails
|
||||
// about that change.
|
||||
async function updateUserEmailAddress(ctx, email, confirmPassword) {
|
||||
const {
|
||||
user,
|
||||
loaders: { Settings },
|
||||
connectors: { models: { User }, services: { Mailer, I18n, Users } },
|
||||
} = ctx;
|
||||
|
||||
// Ensure that the user has a local profile associated with their account.
|
||||
if (!hasLocalProfile(user)) {
|
||||
throw new ErrNoLocalProfile();
|
||||
}
|
||||
|
||||
// Ensure that the password provided matches what we have on file.
|
||||
if (!await user.verifyPassword(confirmPassword)) {
|
||||
throw new ErrIncorrectPassword();
|
||||
}
|
||||
|
||||
// Cleanup the email address.
|
||||
email = email.toLowerCase().trim();
|
||||
|
||||
// Update the Users email address.
|
||||
await User.update(
|
||||
{
|
||||
id: user.id,
|
||||
profiles: { $elemMatch: { provider: 'local' } },
|
||||
},
|
||||
{
|
||||
$set: { 'profiles.$.id': email },
|
||||
$unset: { 'profiles.$.metadata.confirmed_at': 1 },
|
||||
}
|
||||
);
|
||||
|
||||
// Get some context for the email to be sent.
|
||||
const { organizationContactEmail } = await Settings.load([
|
||||
'organizationContactEmail',
|
||||
]);
|
||||
|
||||
// Send off the email to the old email address that we have changed it.
|
||||
await Mailer.send({
|
||||
email: user.firstEmail,
|
||||
template: 'plain',
|
||||
locals: {
|
||||
body: I18n.t(
|
||||
'email.email_change_original.body',
|
||||
user.firstEmail,
|
||||
email,
|
||||
organizationContactEmail
|
||||
),
|
||||
},
|
||||
subject: I18n.t('email.email_change_original.subject'),
|
||||
});
|
||||
|
||||
// Send off the email to the new email address that we need to verify the new
|
||||
// address.
|
||||
await Users.sendEmailConfirmation(user, email);
|
||||
}
|
||||
|
||||
// attachUserLocalAuth will attach a new local profile to an existing user.
|
||||
async function attachUserLocalAuth(ctx, email, password) {
|
||||
const { user, connectors: { models: { User }, services: { Users } } } = ctx;
|
||||
|
||||
// Ensure that the current user doesn't already have a local account
|
||||
// associated with them.
|
||||
if (hasLocalProfile(user)) {
|
||||
throw new ErrLocalProfile();
|
||||
}
|
||||
|
||||
// Cleanup the email address.
|
||||
email = email.toLowerCase().trim();
|
||||
|
||||
// Validate the password.
|
||||
await Users.isValidPassword(password);
|
||||
|
||||
// Hash the new password.
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
try {
|
||||
// Associate the account with the user.
|
||||
const updatedUser = await User.findOneAndUpdate(
|
||||
{
|
||||
id: user.id,
|
||||
'profiles.provider': { $ne: 'local' },
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
profiles: {
|
||||
provider: 'local',
|
||||
id: email,
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
if (!updatedUser) {
|
||||
const foundUser = await User.findOne({ id: user.id });
|
||||
if (!foundUser) {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Check to see if this was the result of a race.
|
||||
if (hasLocalProfile(foundUser)) {
|
||||
throw new ErrLocalProfile();
|
||||
}
|
||||
|
||||
throw new Error('local auth attachment failed due to unexpected reason');
|
||||
}
|
||||
|
||||
// Send off the email to the new email address that we need to verify the
|
||||
// new address.
|
||||
await Users.sendEmailConfirmation(updatedUser, email);
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
throw new ErrEmailTaken();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ctx => {
|
||||
const mutators = {
|
||||
User: {
|
||||
updateEmailAddress: () => Promise.reject(new ErrNotAuthorized()),
|
||||
attachLocalAuth: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
if (ctx.user) {
|
||||
mutators.User.updateEmailAddress = ({ email, confirmPassword }) =>
|
||||
updateUserEmailAddress(ctx, email, confirmPassword);
|
||||
|
||||
mutators.User.attachLocalAuth = ({ email, password }) =>
|
||||
attachUserLocalAuth(ctx, email, password);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
updateEmailAddress: async (root, { input }, { mutators: { User } }) => {
|
||||
await User.updateEmailAddress(input);
|
||||
},
|
||||
attachLocalAuth: async (root, { input }, { mutators: { User } }) => {
|
||||
await User.attachLocalAuth(input);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
en:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: Email change
|
||||
body: Your email address has been changed from {0} to {1}. If you did not initiate this change, please contact {2}. # TODO: update translation
|
||||
error:
|
||||
NO_LOCAL_PROFILE: No existing email address is associated with this account.
|
||||
LOCAL_PROFILE: An email address is already associated with this account.
|
||||
INCORRECT_PASSWORD: Provided password was incorrect.
|
||||
@@ -0,0 +1,47 @@
|
||||
# UpdateEmailAddressInput provides input for changing a users email address
|
||||
# associated with their account.
|
||||
input UpdateEmailAddressInput {
|
||||
|
||||
# email is the Users email address that they want to update to.
|
||||
email: String!
|
||||
|
||||
# confirmPassword is the Users current password.
|
||||
confirmPassword: String!
|
||||
}
|
||||
|
||||
# UpdateEmailAddressResponse is returned when you try to update a users email
|
||||
# address.
|
||||
type UpdateEmailAddressResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# AttachLocalAuthInput provides the input for attaching a new local
|
||||
# authentication profile.
|
||||
input AttachLocalAuthInput {
|
||||
|
||||
# email is the Users email address that they want to add.
|
||||
email: String!
|
||||
|
||||
# password is the Users password that they want to add.
|
||||
password: String!
|
||||
}
|
||||
|
||||
# AttachLocalAuthResponse returns any errors for when the user attempts to
|
||||
# attach a new local authentication profile.
|
||||
type AttachLocalAuthResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# updateEmailAddress changes the email address of the current logged in user.
|
||||
updateEmailAddress(input: UpdateEmailAddressInput!): UpdateEmailAddressResponse
|
||||
|
||||
# attachLocalAuth will attach a new local authentication profile to an
|
||||
# account.
|
||||
attachLocalAuth(input: AttachLocalAuthInput!): AttachLocalAuthResponse
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = fs.readFileSync(
|
||||
path.join(__dirname, 'typeDefs.graphql'),
|
||||
'utf8'
|
||||
);
|
||||
@@ -21,4 +21,13 @@ that contains a download link. Only one link can be generated every 7 days, and
|
||||
the link will be valid for 24 hours.
|
||||
|
||||
The downloaded zip file will contain all the users comments in a CSV format
|
||||
including those that have been rejected, withheld, or still in premod.
|
||||
including those that have been rejected, withheld, or still in pre-moderation.
|
||||
|
||||
## 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. This
|
||||
plugin can work with its client plugin disabled and then directly integrated
|
||||
with existing workflows for an organization of any size through use of the API
|
||||
that this plugin provides.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
.container {
|
||||
border: 1px solid #f26563;
|
||||
border-radius: 2px;
|
||||
color: #3b4a53;
|
||||
padding: 20px 10px;
|
||||
background-color: rgba(242, 101, 99, 0.1);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #787D80;
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: #787D80;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
i.icon {
|
||||
font-size: 1em;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
padding-left: 22px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import moment from 'moment';
|
||||
import { Button, Icon } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './AccountDeletionRequestedSign.css';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import { scheduledDeletionDelayHours } from '../../config';
|
||||
|
||||
class AccountDeletionRequestedSign extends React.Component {
|
||||
cancelAccountDeletion = async () => {
|
||||
const { cancelAccountDeletion, notify } = this.props;
|
||||
try {
|
||||
await cancelAccountDeletion();
|
||||
notify('success', t('delete_request.account_deletion_cancelled'));
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { me: { scheduledDeletionDate } } = this.props.root;
|
||||
|
||||
const deletionScheduledFor = moment(scheduledDeletionDate).format(
|
||||
'MMM Do YYYY, h:mm a'
|
||||
);
|
||||
const deletionScheduledOn = moment(scheduledDeletionDate)
|
||||
.subtract(scheduledDeletionDelayHours, 'hours')
|
||||
.format('MMM Do YYYY, h:mm a');
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h4 className={styles.title}>
|
||||
<Icon name="warning" className={styles.icon} />{' '}
|
||||
{t('delete_request.account_deletion_requested')}
|
||||
</h4>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.received_on')}
|
||||
{deletionScheduledFor}.
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.cancel_request_description')}
|
||||
<b>
|
||||
{' '}
|
||||
{t('delete_request.before')} {deletionScheduledOn}
|
||||
</b>.
|
||||
</p>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.secondary)}
|
||||
onClick={this.cancelAccountDeletion}
|
||||
>
|
||||
{t('delete_request.cancel_account_deletion_request')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccountDeletionRequestedSign.propTypes = {
|
||||
cancelAccountDeletion: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default AccountDeletionRequestedSign;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import DeleteMyAccountDialog from './DeleteMyAccountDialog';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const initialState = { showDialog: false };
|
||||
|
||||
class DeleteMyAccount extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
showDialog = () => {
|
||||
this.setState({
|
||||
showDialog: true,
|
||||
});
|
||||
};
|
||||
|
||||
closeDialog = () => {
|
||||
this.setState({
|
||||
showDialog: false,
|
||||
});
|
||||
};
|
||||
|
||||
cancelAccountDeletion = async () => {
|
||||
const { cancelAccountDeletion, notify } = this.props;
|
||||
try {
|
||||
await cancelAccountDeletion();
|
||||
notify('success', t('delete_request.account_deletion_requested'));
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
requestAccountDeletion = async () => {
|
||||
const { requestAccountDeletion, notify } = this.props;
|
||||
try {
|
||||
await requestAccountDeletion();
|
||||
notify('success', t('delete_request.account_deletion_requested'));
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
me: { scheduledDeletionDate },
|
||||
settings: { organizationContactEmail },
|
||||
} = this.props.root;
|
||||
return (
|
||||
<div className="talk-plugin-auth--delete-my-account">
|
||||
<DeleteMyAccountDialog
|
||||
requestAccountDeletion={this.requestAccountDeletion}
|
||||
showDialog={this.state.showDialog}
|
||||
closeDialog={this.closeDialog}
|
||||
scheduledDeletionDate={scheduledDeletionDate}
|
||||
organizationContactEmail={organizationContactEmail}
|
||||
/>
|
||||
<h3 className="talk-plugin-auth--delete-my-account-description">
|
||||
{t('delete_request.delete_my_account')}
|
||||
</h3>
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{t('delete_request.delete_my_account_description')}
|
||||
</p>
|
||||
<p className="talk-plugin-auth--delete-my-account-description">
|
||||
{scheduledDeletionDate &&
|
||||
t(
|
||||
'delete_request.already_submitted_request_description',
|
||||
moment(scheduledDeletionDate).format('MMM Do YYYY, h:mm:ss a')
|
||||
)}
|
||||
</p>
|
||||
{scheduledDeletionDate ? (
|
||||
<Button onClick={this.cancelAccountDeletion}>
|
||||
{t('delete_request.cancel_account_deletion_request')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button icon="delete" onClick={this.showDialog}>
|
||||
{t('delete_request.delete_my_account')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteMyAccount.propTypes = {
|
||||
requestAccountDeletion: PropTypes.func.isRequired,
|
||||
cancelAccountDeletion: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccount;
|
||||
@@ -0,0 +1,38 @@
|
||||
.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: 380px;
|
||||
top: 10px;
|
||||
font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 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.2em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1em;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './DeleteMyAccountDialog.css';
|
||||
import { Dialog } from 'plugin-api/beta/client/components/ui';
|
||||
import StepProgress from './StepProgress';
|
||||
import DeleteMyAccountStep0 from './DeleteMyAccountStep0';
|
||||
import DeleteMyAccountStep1 from './DeleteMyAccountStep1';
|
||||
import DeleteMyAccountStep2 from './DeleteMyAccountStep2';
|
||||
import DeleteMyAccountStep3 from './DeleteMyAccountStep3';
|
||||
import DeleteMyAccountFinalStep from './DeleteMyAccountFinalStep';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const initialState = { step: 0, formData: {} };
|
||||
|
||||
class DeleteMyAccountDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
goToNextStep = () => {
|
||||
this.setState(state => ({
|
||||
step: state.step + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.setState(initialState);
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.clear();
|
||||
this.props.closeDialog();
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
this.setState(state => ({
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { step } = this.state;
|
||||
const { scheduledDeletionDate, organizationContactEmail } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog open={this.props.showDialog} className={styles.dialog}>
|
||||
<span className={styles.close} onClick={this.cancel}>
|
||||
×
|
||||
</span>
|
||||
<h3 className={styles.title}>
|
||||
{t('delete_request.delete_my_account')}
|
||||
</h3>
|
||||
<StepProgress currentStep={this.state.step} totalSteps={4} />
|
||||
{step === 0 && (
|
||||
<DeleteMyAccountStep0
|
||||
goToNextStep={this.goToNextStep}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
)}
|
||||
{step === 1 && (
|
||||
<DeleteMyAccountStep1
|
||||
goToNextStep={this.goToNextStep}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<DeleteMyAccountStep2
|
||||
goToNextStep={this.goToNextStep}
|
||||
cancel={this.cancel}
|
||||
/>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<DeleteMyAccountStep3
|
||||
formData={this.state.formData}
|
||||
goToNextStep={this.goToNextStep}
|
||||
cancel={this.cancel}
|
||||
requestAccountDeletion={this.props.requestAccountDeletion}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
)}
|
||||
{step === 4 && (
|
||||
<DeleteMyAccountFinalStep
|
||||
scheduledDeletionDate={scheduledDeletionDate}
|
||||
organizationContactEmail={organizationContactEmail}
|
||||
finish={this.cancel}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteMyAccountDialog.propTypes = {
|
||||
showDialog: PropTypes.bool.isRequired,
|
||||
closeDialog: PropTypes.func.isRequired,
|
||||
requestAccountDeletion: PropTypes.func.isRequired,
|
||||
scheduledDeletionDate: PropTypes.any,
|
||||
organizationContactEmail: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccountDialog;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './DeleteMyAccountStep.css';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import moment from 'moment';
|
||||
|
||||
const DeleteMyAccountFinalStep = props => (
|
||||
<div className={styles.step}>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.your_request_submitted_description')}
|
||||
</p>
|
||||
|
||||
<div className={cn(styles.box, styles.scheduledDeletion)}>
|
||||
<strong className={styles.block}>
|
||||
{t('delete_request.your_account_deletion_scheduled')}
|
||||
</strong>
|
||||
<strong className={styles.block}>
|
||||
<Icon name="access_time" className={styles.timeIcon} />
|
||||
<span>
|
||||
{moment(props.scheduledDeletionDate).format('MMM Do YYYY, h:mm a')}
|
||||
</span>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<p className={styles.description}>
|
||||
<strong> {t('delete_request.changed_your_mind')}</strong>{' '}
|
||||
{t('delete_request.simply_go_to')} “<strong>
|
||||
{t('delete_request.cancel_account_deletion_request')}.
|
||||
</strong>”
|
||||
</p>
|
||||
|
||||
<p className={styles.description}>
|
||||
<strong>{t('delete_request.tell_us_why')}.</strong>{' '}
|
||||
{t('delete_request.feedback_copy')}{' '}
|
||||
<a href={`mailto:${props.organizationContactEmail}`}>
|
||||
{props.organizationContactEmail}
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
<div className={cn(styles.actions, styles.columnView)}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.proceed)}
|
||||
onClick={props.finish}
|
||||
full
|
||||
>
|
||||
{t('delete_request.done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
DeleteMyAccountFinalStep.propTypes = {
|
||||
finish: PropTypes.func.isRequired,
|
||||
scheduledDeletionDate: PropTypes.any.isRequired,
|
||||
organizationContactEmail: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccountFinalStep;
|
||||
@@ -0,0 +1,116 @@
|
||||
.list {
|
||||
padding: 0;
|
||||
margin: 20px 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.itemIcon {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-grow: 1;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
> i.itemIcon {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #787D80;
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
background-color: transparent;
|
||||
color: #787D80;
|
||||
}
|
||||
|
||||
&.proceed {
|
||||
background-color: #3498DB;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: #FA4643;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
padding-top: 20px;
|
||||
|
||||
&.columnView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1em;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
font-size: 1em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.textBox {
|
||||
background-color: #F1F2F2;
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
color: #3B4A53;
|
||||
font-size: 1em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.step {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.scheduledDeletion {
|
||||
i.timeIcon {
|
||||
font-size: 1.2em;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button, Icon } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './DeleteMyAccountStep.css';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const DeleteMyAccountStep0 = props => (
|
||||
<div className={styles.step}>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.step_0.you_are_attempting')}
|
||||
</p>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.item}>
|
||||
<Icon name="done" className={styles.itemIcon} />
|
||||
<span className={styles.text}>{t('delete_request.step_0.item_1')}</span>
|
||||
</li>
|
||||
<li className={styles.item}>
|
||||
<Icon name="done" className={styles.itemIcon} />
|
||||
<span className={styles.text}>{t('delete_request.step_0.item_2')}</span>
|
||||
</li>
|
||||
<li className={styles.item}>
|
||||
<Icon name="done" className={styles.itemIcon} />
|
||||
<span className={styles.text}>{t('delete_request.step_0.item_3')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={cn(styles.actions)}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.cancel)}
|
||||
onClick={props.cancel}
|
||||
>
|
||||
{t('delete_request.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.button, styles.proceed)}
|
||||
onClick={props.goToNextStep}
|
||||
>
|
||||
{t('delete_request.proceed')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
DeleteMyAccountStep0.propTypes = {
|
||||
goToNextStep: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccountStep0;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './DeleteMyAccountStep.css';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { scheduledDeletionDelayHours } from '../../config';
|
||||
|
||||
const DeleteMyAccountStep1 = props => (
|
||||
<div className={styles.step}>
|
||||
<h4 className={styles.subTitle}>{t('delete_request.step_1.subtitle')}</h4>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.step_1.description', scheduledDeletionDelayHours)}
|
||||
</p>
|
||||
<h4 className={styles.subTitle}>{t('delete_request.step_1.subtitle_2')}</h4>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.step_1.description_2', scheduledDeletionDelayHours)}
|
||||
</p>
|
||||
<div className={cn(styles.actions)}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.cancel)}
|
||||
onClick={props.cancel}
|
||||
>
|
||||
{t('delete_request.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.button, styles.proceed)}
|
||||
onClick={props.goToNextStep}
|
||||
>
|
||||
{t('delete_request.proceed')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
DeleteMyAccountStep1.propTypes = {
|
||||
goToNextStep: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccountStep1;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './DeleteMyAccountStep.css';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const DeleteMyAccountStep2 = props => (
|
||||
<div className={styles.step}>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.step_2.description')}
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.step_2.to_download')}
|
||||
<strong className={styles.block}>
|
||||
{t('delete_request.step_2.path')}
|
||||
</strong>
|
||||
</p>
|
||||
<div className={cn(styles.actions)}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.cancel)}
|
||||
onClick={props.cancel}
|
||||
>
|
||||
{t('delete_request.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.button, styles.proceed)}
|
||||
onClick={props.goToNextStep}
|
||||
>
|
||||
{t('delete_request.proceed')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
DeleteMyAccountStep2.propTypes = {
|
||||
goToNextStep: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccountStep2;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './DeleteMyAccountStep.css';
|
||||
import InputField from './InputField';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const initialState = {
|
||||
showError: false,
|
||||
};
|
||||
|
||||
class DeleteMyAccountStep3 extends React.Component {
|
||||
state = initialState;
|
||||
phrase = 'delete';
|
||||
|
||||
showError = () => {
|
||||
this.setState({
|
||||
showError: true,
|
||||
});
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.setState(initialState);
|
||||
};
|
||||
|
||||
deleteAndContinue = async () => {
|
||||
if (this.formHasError()) {
|
||||
this.showError();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.props.requestAccountDeletion();
|
||||
this.clear();
|
||||
this.props.goToNextStep();
|
||||
};
|
||||
|
||||
formHasError = () =>
|
||||
!this.props.formData.confirmPhrase ||
|
||||
this.props.formData.confirmPhrase !== this.phrase;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<h4 className={styles.subTitle}>
|
||||
{t('delete_request.step_3.subtitle')}
|
||||
</h4>
|
||||
<p className={styles.description}>
|
||||
{t('delete_request.step_3.description')}
|
||||
</p>
|
||||
<input
|
||||
className={styles.textBox}
|
||||
disabled={true}
|
||||
readOnly={true}
|
||||
value={this.phrase}
|
||||
/>
|
||||
<InputField
|
||||
id="confirmPhrase"
|
||||
label={t('delete_request.step_3.type_to_confirm')}
|
||||
name="confirmPhrase"
|
||||
type="text"
|
||||
onChange={this.props.onChange}
|
||||
defaultValue=""
|
||||
hasError={this.formHasError()}
|
||||
errorMsg={t('delete_request.input_is_not_correct')}
|
||||
showError={this.state.showError}
|
||||
columnDisplay
|
||||
/>
|
||||
<div className={cn(styles.actions)}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.cancel)}
|
||||
onClick={this.props.cancel}
|
||||
>
|
||||
{t('delete_request.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.button, styles.danger)}
|
||||
onClick={this.deleteAndContinue}
|
||||
>
|
||||
{t('delete_request.delete_my_account')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteMyAccountStep3.propTypes = {
|
||||
goToNextStep: PropTypes.func.isRequired,
|
||||
requestAccountDeletion: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
formData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteMyAccountStep3;
|
||||
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './DownloadCommentHistory.css';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import { downloadRateLimitDays } from '../../config';
|
||||
|
||||
export const readableDuration = durAsHours => {
|
||||
const durAsDays = Math.ceil(durAsHours / 24);
|
||||
@@ -19,21 +21,30 @@ export const readableDuration = durAsHours => {
|
||||
class DownloadCommentHistory extends Component {
|
||||
static propTypes = {
|
||||
requestDownloadLink: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
requestDownloadLink = async () => {
|
||||
const { requestDownloadLink, notify } = this.props;
|
||||
try {
|
||||
await requestDownloadLink();
|
||||
notify('success', t('download_request.download_preparing'));
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
root: { me: { lastAccountDownload } },
|
||||
requestDownloadLink,
|
||||
} = this.props;
|
||||
const { root: { me: { lastAccountDownload } } } = this.props;
|
||||
|
||||
const now = new Date();
|
||||
const lastAccountDownloadDate =
|
||||
lastAccountDownload && new Date(lastAccountDownload);
|
||||
const hoursLeft = lastAccountDownloadDate
|
||||
? Math.ceil(
|
||||
7 * 24 - (now.getTime() - lastAccountDownloadDate.getTime()) / 3.6e6
|
||||
downloadRateLimitDays * 24 -
|
||||
(now.getTime() - lastAccountDownloadDate.getTime()) / 3.6e6
|
||||
)
|
||||
: 0;
|
||||
const canRequestDownload = !lastAccountDownloadDate || hoursLeft <= 0;
|
||||
@@ -43,7 +54,7 @@ class DownloadCommentHistory extends Component {
|
||||
<h3>{t('download_request.section_title')}</h3>
|
||||
<p>
|
||||
{t('download_request.you_will_get_a_copy')}{' '}
|
||||
<b>{t('download_request.download_rate')}</b>.
|
||||
<b>{t('download_request.download_rate', downloadRateLimitDays)}</b>.
|
||||
</p>
|
||||
{lastAccountDownloadDate && (
|
||||
<p className={styles.most_recent}>
|
||||
@@ -52,7 +63,7 @@ class DownloadCommentHistory extends Component {
|
||||
</p>
|
||||
)}
|
||||
{canRequestDownload ? (
|
||||
<Button className={styles.button} onClick={requestDownloadLink}>
|
||||
<Button className={styles.button} onClick={this.requestDownloadLink}>
|
||||
<i className="material-icons" aria-hidden={true}>
|
||||
file_download
|
||||
</i>{' '}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.errorMsg {
|
||||
color: #FA4643;
|
||||
font-size: 0.9em;
|
||||
|
||||
i.warningIcon {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
color: #FA4643;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ErrorMessage.css';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const ErrorMessage = ({ children }) => (
|
||||
<div className={styles.errorMsg}>
|
||||
<Icon className={styles.warningIcon} name="warning" />
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ErrorMessage;
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
.detailItem {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detailItemContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.columnDisplay {
|
||||
flex-direction: column;
|
||||
|
||||
.detailItemMessage {
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailItemContent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.detailInput {
|
||||
border: solid 1px #787D80;
|
||||
border-radius: 2px;
|
||||
background-color: white;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
width: 230px;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .detailIcon {
|
||||
font-size: 1.2em;
|
||||
padding: 0 5px;
|
||||
color: #787D80;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: solid 2px #FA4643;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
color: #4C4C4D;
|
||||
font-size: 1em;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1em;
|
||||
color: #000;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.detailItemMessage {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 6px;
|
||||
|
||||
.warningIcon, .checkIcon {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
color: #00CD73;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
color: #FA4643;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './InputField.css';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const InputField = ({
|
||||
id = '',
|
||||
label = '',
|
||||
type = 'text',
|
||||
name = '',
|
||||
onChange = () => {},
|
||||
showError = true,
|
||||
hasError = false,
|
||||
errorMsg = '',
|
||||
children,
|
||||
columnDisplay = false,
|
||||
showSuccess = false,
|
||||
validationType = '',
|
||||
icon = '',
|
||||
value = '',
|
||||
defaultValue = '',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const inputValue = {
|
||||
...(value ? { value } : {}),
|
||||
...(defaultValue ? { defaultValue } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.detailItem}>
|
||||
<div className={cn(styles.detailItemContainer)}>
|
||||
{label && (
|
||||
<label className={styles.detailLabel} id={id}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div
|
||||
className={cn(styles.detailItemContent, {
|
||||
[styles.columnDisplay]: columnDisplay,
|
||||
})}
|
||||
>
|
||||
<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}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InputField.propTypes = {
|
||||
id: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
showError: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
errorMsg: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
columnDisplay: PropTypes.bool,
|
||||
showSuccess: PropTypes.bool,
|
||||
validationType: PropTypes.string,
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
@@ -0,0 +1,36 @@
|
||||
.step {
|
||||
color: #BBBEBF;
|
||||
background-color: white;
|
||||
padding: 6px;
|
||||
z-index: 10;
|
||||
|
||||
> .icon {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
&.current {
|
||||
color: rgba(0, 205, 115, 0.3);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
color: #00CD73;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
border: solid 2px #BBBEBF;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './StepProgress.css';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const CheckItem = ({ current = false, completed = false }) => (
|
||||
<span
|
||||
className={cn(styles.step, {
|
||||
[styles.current]: current,
|
||||
[styles.completed]: completed,
|
||||
})}
|
||||
>
|
||||
<Icon name="check_circle" className={styles.icon} />
|
||||
</span>
|
||||
);
|
||||
|
||||
CheckItem.propTypes = {
|
||||
current: PropTypes.bool.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const Line = () => <hr className={styles.line} />;
|
||||
|
||||
class StepProgress extends React.Component {
|
||||
render() {
|
||||
const { currentStep, totalSteps } = this.props;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{Array.from({ length: totalSteps }).map((_, i) => (
|
||||
<CheckItem
|
||||
key={`step_${i}`}
|
||||
completed={i < currentStep}
|
||||
current={currentStep === i}
|
||||
/>
|
||||
))}
|
||||
<Line />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StepProgress.propTypes = {
|
||||
currentStep: PropTypes.number.isRequired,
|
||||
totalSteps: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default StepProgress;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import AccountDeletionRequestedSign from '../components/AccountDeletionRequestedSign';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { withCancelAccountDeletion } from '../hocs';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
const withData = withFragments({
|
||||
root: gql`
|
||||
fragment Talk_AccountDeletionRequestedSignIn_root on RootQuery {
|
||||
me {
|
||||
scheduledDeletionDate
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withCancelAccountDeletion,
|
||||
withData,
|
||||
excludeIf(({ root: { me } }) => !me || !me.scheduledDeletionDate)
|
||||
)(AccountDeletionRequestedSign);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import DeleteMyAccount from '../components/DeleteMyAccount';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { withRequestAccountDeletion, withCancelAccountDeletion } from '../hocs';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
const withData = withFragments({
|
||||
root: gql`
|
||||
fragment Talk_DeleteMyAccount_root on RootQuery {
|
||||
me {
|
||||
scheduledDeletionDate
|
||||
}
|
||||
settings {
|
||||
organizationContactEmail
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withRequestAccountDeletion,
|
||||
withCancelAccountDeletion,
|
||||
withData
|
||||
)(DeleteMyAccount);
|
||||
@@ -1,27 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import DownloadCommentHistory from '../components/DownloadCommentHistory';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { withRequestDownloadLink } from '../mutations';
|
||||
import { withRequestDownloadLink } from '../hocs';
|
||||
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
class DownloadCommentHistoryContainer extends Component {
|
||||
static propTypes = {
|
||||
requestDownloadLink: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DownloadCommentHistory
|
||||
root={this.props.root}
|
||||
notify={this.props.notify}
|
||||
requestDownloadLink={this.props.requestDownloadLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkDownloadCommentHistory_DownloadCommentHistorySection_root on RootQuery {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user