Compare commits

...
95 Commits
Author SHA1 Message Date
Wyatt Johnson 74d38f16ed Merge pull request #1388 from coralproject/bump-version-4-2-2
Update version to 4.2.2
2018-02-20 10:27:14 -07:00
Wyatt Johnson 7129896bd6 Merge branch 'master' into bump-version-4-2-2 2018-02-20 09:57:12 -07:00
Wyatt Johnson e2e718494c Merge pull request #1387 from coralproject/action-fixes
Action Count Fixes
2018-02-20 09:57:05 -07:00
Kim Gardner f918bbea20 Update version to 4.2.2 2018-02-20 11:45:36 -05:00
Wyatt Johnson 4467f6110f Merge branch 'master' into action-fixes 2018-02-20 09:44:00 -07:00
Wyatt Johnson a166404347 fix for action count null 2018-02-20 09:31:08 -07:00
Kim Gardner 3d2f3ebc59 Merge pull request #1384 from coralproject/user-tooltip-style
Making the User Info Tooltip more readable and friendly
2018-02-20 10:56:04 -05:00
Kim Gardner c4cdc672b7 Merge branch 'master' into user-tooltip-style 2018-02-20 10:44:15 -05:00
Kim Gardner b84a488518 Merge pull request #1380 from leeeandroo/facebook-translations
Fix translations keys and add german translations to facebook plugin
2018-02-20 10:44:05 -05:00
Kim Gardner 4b134cbfc2 Merge branch 'master' into facebook-translations 2018-02-20 10:34:22 -05:00
Kim Gardner c446098a1e Merge branch 'master' into user-tooltip-style 2018-02-20 10:24:00 -05:00
Kim Gardner b26e938ac5 Merge pull request #1381 from leeeandroo/package-version421
set a right last release 4.2.1 to package.json
2018-02-20 10:22:09 -05:00
okbel 846df0a5e0 trim dates, style tooltip 2018-02-19 12:27:55 -03:00
okbel e7934d9468 trim dates, style tooltip 2018-02-19 12:23:36 -03:00
Leandro 606d5734f9 set a right last release 4.2.1 to package.json 2018-02-19 11:18:56 +01:00
Leandro 87ff631eed fix translations keys and add germna translations to facebook plugin 2018-02-19 09:50:07 +01:00
Kim Gardner 950efba3fe Merge pull request #1378 from coralproject/suspension-fix
Suspension Button Fix
2018-02-16 18:27:48 -05:00
Wyatt Johnson af592e326f Merge branch 'master' into suspension-fix 2018-02-16 16:18:02 -07:00
Wyatt Johnson c2aa2077f4 Show suspension status on button 2018-02-16 16:14:49 -07:00
Kim Gardner 26476f42f7 Merge pull request #1377 from coralproject/template-fixes
Template bugs
2018-02-16 18:06:28 -05:00
Wyatt Johnson 3434fb4fed Merge branch 'master' into template-fixes 2018-02-16 15:45:14 -07:00
Wyatt Johnson a6bc29af9a Merge pull request #1368 from coralproject/snyk-fix-946937f7
[Snyk Update] New fixes for 4 vulnerable dependency paths
2018-02-16 15:34:54 -07:00
Wyatt Johnson d86ff8418d fixed changes to api 2018-02-16 15:26:33 -07:00
Kim Gardner e71bcbe58d Yarn install 2018-02-16 17:14:54 -05:00
Kim Gardner 77c838ea22 Merge branch 'master' into snyk-fix-946937f7 2018-02-16 17:03:50 -05:00
Kim Gardner 39313a20fa Merge pull request #1374 from coralproject/spam-fixes
Fixes for Spam Plugin
2018-02-16 16:51:37 -05:00
Wyatt Johnson ad4427286d Merge branch 'master' into snyk-fix-946937f7 2018-02-16 14:39:32 -07:00
Wyatt Johnson dc82db6eda fixed template bugs 2018-02-16 14:38:57 -07:00
Wyatt Johnson 4bb2b3d731 fixes to spam plugin 2018-02-16 14:34:04 -07:00
Kim Gardner d812a0f696 Merge pull request #1371 from coralproject/fix-auth-proptypes
Fix proptypes errors in auth
2018-02-16 10:58:22 -05:00
Chi Vinh Le ba11eb233a Fix proptypes errors in auth 2018-02-16 12:31:37 +01:00
Wyatt Johnson e76d109f93 Merge branch 'master' into snyk-fix-946937f7 2018-02-15 14:23:28 -07:00
Kim Gardner 78db4077e1 Merge pull request #1369 from coralproject/typo-breaks-akismet
Translation typo is breaking Akismet
2018-02-15 16:07:00 -05:00
Kim Gardner 0519b55707 Translation typo is breaking Akismet 2018-02-15 15:55:51 -05:00
Kim Gardner 5602bf222c Merge pull request #1365 from coralproject/no-limit
Configurable Limits
2018-02-15 15:12:28 -05:00
Kim Gardner bfd6cfbaa4 Merge branch 'master' into snyk-fix-946937f7 2018-02-14 21:03:45 -05:00
Kim Gardner 9ec7027f3c Merge branch 'master' into no-limit 2018-02-14 21:03:17 -05:00
Kim Gardner 4acb6aeb6c Merge pull request #1361 from coralproject/auth-refactor
Auth refactor
2018-02-14 21:01:21 -05:00
snyk-bot c3bccaf4a8 fix: package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/npm:hoek:20180212
- https://snyk.io/vuln/npm:hoek:20180212

Latest report for coralproject/talk:
https://snyk.io/test/github/coralproject/talk
2018-02-15 01:19:30 +00:00
Kim Gardner e162a97919 Remove Facebook Auth from default Talk plugins 2018-02-14 18:31:12 -05:00
Kim Gardner f63a7915ea Update Facebook Auth and Recaptcha Docs 2018-02-14 18:24:16 -05:00
Wyatt Johnson 18710e2345 added tests 2018-02-14 15:58:15 -07:00
Kim Gardner ff29fa04e5 Merge branch 'master' into auth-refactor 2018-02-14 17:55:06 -05:00
Kim Gardner 7a9023744a Merge pull request #1363 from coralproject/router-cleanup
Router Cleanup
2018-02-14 17:54:45 -05:00
Kim Gardner b1c03973e6 Merge branch 'master' into router-cleanup 2018-02-14 17:39:39 -05:00
Kiwi 9e4759cbfd Merge branch 'master' into auth-refactor 2018-02-14 20:14:22 +01:00
Kiwi 94c98f000c Merge branch 'master' into no-limit 2018-02-14 20:11:39 +01:00
Wyatt Johnson c2154b6efa Merge branch 'master' into router-cleanup 2018-02-14 10:44:43 -07:00
Kiwi c46b29aed9 Merge branch 'master' into router-cleanup 2018-02-14 16:06:59 +01:00
Chi Vinh Le 43e02e1574 Apply suggestions 2018-02-14 14:59:00 +01:00
Wyatt Johnson ffd88e0ef0 Merge branch 'master' into no-limit 2018-02-13 18:32:02 -07:00
Wyatt Johnson 9820e575b2 patch 2018-02-13 18:29:07 -07:00
Wyatt Johnson 1e329c8967 code cleanup 2018-02-13 17:45:26 -07:00
Wyatt Johnson 73034a20f0 added support for configuring limiting, action optims 2018-02-13 17:24:57 -07:00
Wyatt Johnson 8f7171b54e fixed old default.css 2018-02-13 13:59:20 -07:00
Chi Vinh Le b6e4581ba2 Merge branch 'auth-refactor' of ssh://github.com/coralproject/talk into auth-refactor 2018-02-13 21:27:13 +01:00
Chi Vinh Le 78444a0bb4 Remove empty reducers 2018-02-13 21:25:48 +01:00
Chi Vinh Le 4d7e64e1ec Fix css 2018-02-13 21:20:08 +01:00
Kiwi b8e340c471 Merge branch 'master' into auth-refactor 2018-02-13 21:19:50 +01:00
Chi Vinh Le 39c1fcf83a remove before close 2018-02-13 21:17:41 +01:00
Chi Vinh Le 4b2d9fc93a Add formatting to Slot 2018-02-13 21:17:24 +01:00
Wyatt Johnson 0a324a2268 template cleanup 2018-02-13 12:46:55 -07:00
Wyatt Johnson fc27e02140 removed old unreverted code 2018-02-13 12:28:42 -07:00
Wyatt Johnson f40f209395 cleaned up routes 2018-02-13 12:12:03 -07:00
Wyatt Johnson e924ed9189 reverted path change, cleaned embed stream 2018-02-13 11:35:20 -07:00
Wyatt Johnson 4053a25239 moved login route from /login to /embed/login 2018-02-13 11:35:20 -07:00
Kim Gardner de094ee948 Merge branch 'master' into auth-refactor 2018-02-13 13:07:42 -05:00
Chi Vinh Le c7ee8307e6 Merge branch 'master' into auth-refactor 2018-02-13 16:33:17 +01:00
Chi Vinh Le 431bbd4f03 Fix some comments 2018-02-13 16:03:06 +01:00
Chi Vinh Le 0f69df26f6 Use more selectors 2018-02-13 16:00:03 +01:00
Chi Vinh Le c140a62822 Fix e2e 2018-02-13 15:53:12 +01:00
Chi Vinh Le cad25eedd1 CSS cleanup 2018-02-13 15:46:49 +01:00
Chi Vinh Le 87af05c0d9 Fix translations 2018-02-13 15:33:50 +01:00
Chi Vinh Le c54a8bdc4f Remove old code 2018-02-13 15:00:53 +01:00
Chi Vinh Le d857585c7f Implement facebook auth client 2018-02-13 12:26:55 +01:00
Chi Vinh Le 3999b806c7 Use plugin-api 2018-02-13 12:24:29 +01:00
Chi Vinh Le 6c1ed826d0 Fix External 2018-02-13 11:48:12 +01:00
Chi Vinh Le 4224f18ba7 Add BareButton 2018-02-13 11:42:25 +01:00
Chi Vinh Le 6feaa94fba Listen for auth changes 2018-02-13 11:34:30 +01:00
Chi Vinh Le d639596047 Add External Slot 2018-02-13 11:26:37 +01:00
Chi Vinh Le f5409b11bf Use plugin-api 2018-02-13 11:14:12 +01:00
Chi Vinh Le 142c5b32f1 More refactoring 2018-02-13 00:54:15 +01:00
Chi Vinh Le 4b6a3b5b9d More refactoring 2018-02-09 23:18:21 +01:00
Chi Vinh Le f4fc8915e3 Refactor talk-plugin-auth part 1 2018-02-09 20:12:02 +01:00
Chi Vinh Le 1774f98519 Refactor Stream container passing props 2018-02-09 00:19:22 +01:00
Chi Vinh Le 4883bb17b9 Finish refactor embed stream auth 2018-02-09 00:08:31 +01:00
Chi Vinh Le 37dd6ae044 Auth refactor part 3 2018-02-08 23:06:01 +01:00
Chi Vinh Le d526a404ae Finish admin auth refactor 2018-02-07 22:17:52 +01:00
Chi Vinh Le aaad7aa86f Reimplement recaptcha 2018-02-07 22:10:05 +01:00
Wyatt Johnson 74a2ed4a41 introduce no limit 2018-02-07 13:29:59 -07:00
Chi Vinh Le bad0363628 Save static config to redux 2018-02-07 21:08:37 +01:00
Chi Vinh Le ffbcd93c3f Adapt to master changes 2018-02-07 19:04:35 +01:00
Chi Vinh Le 61bc372640 Merge branch 'master' into auth-refactor 2018-02-07 18:42:26 +01:00
Chi Vinh Le ee3d576a49 Auth Refactor part 2 2018-02-07 18:42:03 +01:00
Chi Vinh Le f8e0556904 Auth Refactor part 1 2018-02-06 20:30:31 +01:00
182 changed files with 4151 additions and 3323 deletions
-1
View File
@@ -1,6 +1,5 @@
{
"exec": "npm-run-all --parallel generate-introspection start:development",
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql,yml",
"watch": [
+3
View File
@@ -1,6 +1,9 @@
FROM coralproject/talk:latest
# Setup the build arguments
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
-4
View File
@@ -5,7 +5,6 @@ const uuid = require('uuid');
const merge = require('lodash/merge');
const helmet = require('helmet');
const plugins = require('./services/plugins');
const compression = require('compression');
const { HELMET_CONFIGURATION } = require('./config');
const { MOUNT_PATH } = require('./url');
const routes = require('./routes');
@@ -72,9 +71,6 @@ app.use(
)
);
// Compress the responses if appropriate.
app.use(compression());
//==============================================================================
// VIEW CONFIGURATION
//==============================================================================
-181
View File
@@ -1,181 +0,0 @@
import bowser from 'bowser';
import * as actions from '../constants/auth';
import t from 'coral-framework/services/i18n';
import jwtDecode from 'jwt-decode';
//==============================================================================
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (
dispatch,
_,
{ rest, client, localStorage }
) => {
dispatch({ type: actions.LOGIN_REQUEST });
const params = {
method: 'POST',
body: {
email,
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse,
};
}
return rest('/auth/local', params)
.then(({ user, token }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
return dispatch(checkLoginFailure('not logged in'));
}
dispatch(handleAuthToken(token));
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch({
type: actions.LOGIN_FAILURE,
message: t('error.email_password'),
});
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: t(`error.${error.translation_key}`),
});
} else {
dispatch({
type: actions.LOGIN_FAILURE,
message: errorMessage,
});
}
});
};
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin,
});
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error,
});
export const checkLogin = () => (
dispatch,
_,
{ rest, client, localStorage }
) => {
dispatch(checkLoginRequest());
return rest('/auth')
.then(({ user }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
return dispatch(checkLoginFailure('not logged in'));
}
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
//==============================================================================
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, { rest, client, localStorage }) => {
return rest('/auth', { method: 'DELETE' }).then(() => {
if (localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
// Reset the websocket.
client.resetWebsocket();
dispatch({ type: actions.LOGOUT });
});
};
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('token', token);
}
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
-7
View File
@@ -1,7 +0,0 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({ type: CONFIG_UPDATED, data });
};
+5 -5
View File
@@ -7,12 +7,12 @@ import t from 'coral-framework/services/i18n';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({ handleLogout, auth = {} }) => (
const CoralDrawer = ({ handleLogout, currentUser }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
{can(currentUser, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
@@ -35,7 +35,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
>
{t('configure.community')}
</Link>
{can(auth.user, 'UPDATE_CONFIG') && (
{can(currentUser, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
@@ -55,7 +55,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object,
currentUser: PropTypes.object,
};
export default CoralDrawer;
@@ -0,0 +1,20 @@
.header, .cta, .success {
text-align: center;
font-size: 16px;
}
.success {
cursor: pointer;
padding: 8px 14px;
}
.signInLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.signInLink:hover {
text-decoration: underline;
}
@@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ForgotPassword.css';
import { Button, TextField, Alert, Success } from 'coral-ui';
import t from 'coral-framework/services/i18n';
class ForgotPassword extends React.Component {
constructor(props) {
super(props);
}
handleEmailChange = e => this.props.onEmailChange(e.target.value);
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
};
handleSignInLink = e => {
e.preventDefault();
this.props.onSignInLink();
};
renderSuccess() {
return (
<div className={styles.success} onClick={this.handleSignInLink}>
{t('password_reset.mail_sent')}{' '}
<a
className={styles.signInLink}
href="#"
onClick={this.handleSignInLink}
>
Sign in
</a>
<Success />
</div>
);
}
renderForm() {
const { email, errorMessage } = this.props;
return (
<form onSubmit={this.handleSubmit}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
label="Email Address"
value={email}
onChange={this.handleEmailChange}
/>
<Button type="submit" cStyle="black" full>
Reset Password
</Button>
<p className={styles.cta}>
Go back to{' '}
<a
href="#"
className={styles.signInLink}
onClick={this.handleSignInLink}
>
Sign In
</a>
.
</p>
</form>
);
}
render() {
return this.props.success ? this.renderSuccess() : this.renderForm();
}
}
ForgotPassword.propTypes = {
success: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
onEmailChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
};
export default ForgotPassword;
+13 -11
View File
@@ -13,7 +13,7 @@ import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
currentUser,
root,
data,
}) => {
@@ -22,9 +22,9 @@ const CoralHeader = ({
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
{can(currentUser, 'MODERATE_COMMENTS') && (
<IndexLink
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
@@ -54,7 +54,7 @@ const CoralHeader = ({
<CommunityIndicator root={root} data={data} />
</Link>
{can(auth.user, 'UPDATE_CONFIG') && (
{can(currentUser, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
@@ -97,12 +97,14 @@ const CoralHeader = ({
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
{currentUser && (
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
)}
</Menu>
</div>
</li>
@@ -116,7 +118,7 @@ const CoralHeader = ({
};
CoralHeader.propTypes = {
auth: PropTypes.object,
currentUser: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
+8 -4
View File
@@ -10,22 +10,26 @@ const Layout = ({
handleLogout = () => {},
toggleShortcutModal = () => {},
restricted = false,
auth,
currentUser,
}) => (
<LayoutMDL className={styles.layout} fixedDrawer>
<Header
handleLogout={handleLogout}
showShortcuts={toggleShortcutModal}
auth={auth}
currentUser={currentUser}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
currentUser={currentUser}
/>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
Layout.propTypes = {
children: PropTypes.node,
auth: PropTypes.object,
currentUser: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool, // hide elements from a user that's logged out
@@ -0,0 +1,18 @@
.layout {
max-width: 400px;
margin: 0 auto;
}
.header, .cta {
text-align: center;
font-size: 16px;
}
.layout h1 {
font-size: 40px;
}
.header {
font-size: 30px;
}
@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import SignIn from '../containers/SignIn';
import ForgotPassword from '../containers/ForgotPassword';
import PropTypes from 'prop-types';
import styles from './Login.css';
import Layout from 'coral-admin/src/components/Layout';
import cn from 'classnames';
class LoginContainer extends Component {
renderForm() {
return this.props.forgotPassword ? (
<ForgotPassword onSignInLink={this.props.onSignInLink} />
) : (
<SignIn onForgotPasswordLink={this.props.onForgotPasswordLink} />
);
}
render() {
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.layout, 'talk-admin-login')}>
<h1 className={styles.header}>Team sign in</h1>
<p className={styles.cta}>Sign in to interact with your community.</p>
{this.renderForm()}
</div>
</Layout>
);
}
}
LoginContainer.propTypes = {
forgotPassword: PropTypes.bool.isRequired,
onForgotPasswordLink: PropTypes.func.isRequired,
onSignInLink: PropTypes.func.isRequired,
};
export default LoginContainer;
@@ -3,35 +3,3 @@
margin: 0 auto;
}
.loginLayout {
max-width: 400px;
margin: 0 auto;
}
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
text-align: center;
font-size: 16px;
}
.forgotPasswordLink, .signInLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.forgotPasswordLink:hover, .signInLink:hover {
text-decoration: underline;
}
.layout h1 {
font-size: 40px;
}
.loginHeader {
font-size: 30px;
}
.passwordRequestSuccess {
cursor: pointer;
padding: 8px 14px;
}
@@ -0,0 +1,23 @@
.forgotPasswordCTA {
text-align: center;
font-size: 16px;
}
.forgotPasswordLink:hover {
text-decoration: underline;
}
.forgotPasswordLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.recaptcha {
margin-top: 16px;
margin-bottom: 6px;
}
.signInButton {
margin-top: 10px;
}
@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './SignIn.css';
import { Button, TextField, Alert } from 'coral-ui';
import cn from 'classnames';
import Recaptcha from 'coral-framework/components/Recaptcha';
class SignIn extends React.Component {
recaptcha = null;
handleForgotPasswordLink = e => {
e.preventDefault();
this.props.onForgotPasswordLink();
};
handleEmailChange = e => this.props.onEmailChange(e.target.value);
handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
// Reset recaptcha because each response can only
// be used once.
if (this.recaptcha) {
this.recaptcha.reset();
}
};
handleRecaptchaRef = ref => {
this.recaptcha = ref;
};
render() {
const { email, password, errorMessage, requireRecaptcha } = this.props;
return (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label="Email Address"
value={email}
onChange={this.handleEmailChange}
/>
<TextField
id="password"
label="Password"
value={password}
onChange={this.handlePasswordChange}
type="password"
/>
{requireRecaptcha && (
<div className={styles.recaptcha}>
<Recaptcha
ref={this.handleRecaptchaRef}
onVerify={this.props.onRecaptchaVerify}
/>
</div>
)}
<Button
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
type="submit"
cStyle="black"
full
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={this.handleForgotPasswordLink}
>
Request a new one.
</a>
</p>
</form>
);
}
}
SignIn.propTypes = {
email: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
onEmailChange: PropTypes.func.isRequired,
onPasswordChange: PropTypes.func.isRequired,
onForgotPasswordLink: PropTypes.func.isRequired,
onRecaptchaVerify: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
requireRecaptcha: PropTypes.bool.isRequired,
};
export default SignIn;
@@ -28,8 +28,8 @@
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 10;
top: 32px;
right: 0px;
width: 140px;
left: -100px;
width: 200px;
text-align: left;
color: #616161;
}
@@ -39,7 +39,7 @@
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
left: 96px;
top: -20px;
transform: rotate(180deg);
}
@@ -49,7 +49,7 @@
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
left: 96px;
top: -19px;
transform: rotate(180deg);
}
@@ -67,7 +67,7 @@ class UserInfoTooltip extends React.Component {
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
@@ -139,7 +139,7 @@ class UserInfoTooltip extends React.Component {
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
@@ -154,7 +154,7 @@ class UserInfoTooltip extends React.Component {
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
@@ -0,0 +1,41 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withForgotPassword } from 'coral-framework/hocs';
import { compose } from 'recompose';
import ForgotPassword from '../components/ForgotPassword';
class ForgotPasswordContainer extends Component {
state = {
email: '',
};
handleSubmit = () => {
this.props.forgotPassword(this.state.email);
};
handleEmailChange = email => {
this.setState({ email });
};
render() {
return (
<ForgotPassword
onSubmit={this.handleSubmit}
onEmailChange={this.handleEmailChange}
email={this.state.email}
errorMessage={this.props.errorMessage}
success={this.props.success}
onSignInLink={this.props.onSignInLink}
/>
);
}
}
ForgotPasswordContainer.propTypes = {
success: PropTypes.bool.isRequired,
forgotPassword: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
};
export default compose(withForgotPassword)(ForgotPasswordContainer);
+35 -72
View File
@@ -2,86 +2,58 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/Layout';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import Login from '../containers/Login';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
import {
checkLogin,
handleLogin,
requestPasswordReset,
logout,
} from '../actions/auth';
import { logout } from 'coral-framework/actions/auth';
import { can } from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
import PropTypes from 'prop-types';
class LayoutContainer extends React.Component {
componentWillMount() {
const { checkLogin, fetchConfig } = this.props;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess,
} = this.props.auth;
const {
currentUser,
checkedInitialLogin,
children,
logout,
toggleShortcutModal,
TALK_RECAPTCHA_PUBLIC,
} = this.props;
if (loadingUser) {
if (!checkedInitialLogin) {
return <FullLoading />;
}
if (!loggedIn) {
return (
<AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError}
/>
);
if (!currentUser) {
return <Login />;
}
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
return (
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
auth={this.props.auth}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{children}
</Layout>
);
} else if (loggedIn) {
return (
<Layout handleLogout={logout} {...this.props}>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
if (currentUser) {
if (can(currentUser, 'ACCESS_ADMIN')) {
return (
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
currentUser={this.props.currentUser}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{children}
</Layout>
);
} else {
return (
<Layout {...this.props} handleLogout={logout}>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
}
}
return <FullLoading />;
}
@@ -89,29 +61,20 @@ class LayoutContainer extends React.Component {
LayoutContainer.propTypes = {
children: PropTypes.node,
requestPasswordReset: PropTypes.func,
handleLogin: PropTypes.func,
auth: PropTypes.object,
handleLogout: PropTypes.func,
currentUser: PropTypes.object,
checkedInitialLogin: PropTypes.bool,
logout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func,
};
const mapStateToProps = state => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
currentUser: state.auth.user,
checkedInitialLogin: state.auth.checkedInitialLogin,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout,
},
@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import Login from '../components/Login';
class LoginContainer extends Component {
state = {
forgotPassword: false,
};
switchToForgotPassword = () => {
this.setState({ forgotPassword: true });
};
switchToSignIn = () => {
this.setState({ forgotPassword: false });
};
render() {
return (
<Login
forgotPassword={this.state.forgotPassword}
onForgotPasswordLink={this.switchToForgotPassword}
onSignInLink={this.switchToSignIn}
/>
);
}
}
LoginContainer.propTypes = {};
export default LoginContainer;
@@ -0,0 +1,58 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withSignIn } from 'coral-framework/hocs';
import { compose } from 'recompose';
import SignIn from '../components/SignIn';
class SignInContainer extends Component {
state = {
email: '',
password: '',
recaptchaResponse: null,
};
handleSubmit = () => {
this.props.signIn(
this.state.email,
this.state.password,
this.state.recaptchaResponse
);
};
handleEmailChange = email => {
this.setState({ email });
};
handlePasswordChange = password => {
this.setState({ password });
};
handleRecaptchaVerify = recaptchaResponse => {
this.setState({ recaptchaResponse });
};
render() {
return (
<SignIn
onSubmit={this.handleSubmit}
onEmailChange={this.handleEmailChange}
onPasswordChange={this.handlePasswordChange}
email={this.state.email}
password={this.state.password}
errorMessage={this.props.errorMessage}
onForgotPasswordLink={this.props.onForgotPasswordLink}
onRecaptchaVerify={this.handleRecaptchaVerify}
requireRecaptcha={this.props.requireRecaptcha}
/>
);
}
}
SignInContainer.propTypes = {
signIn: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
onForgotPasswordLink: PropTypes.func.isRequired,
requireRecaptcha: PropTypes.bool.isRequired,
};
export default compose(withSignIn)(SignInContainer);
-65
View File
@@ -1,65 +0,0 @@
import * as actions from '../constants/auth';
const initialState = {
loggedIn: false,
user: null,
loginError: null,
loginMaxExceeded: false,
passwordRequestSuccess: null,
};
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess:
'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default:
return state;
}
}
-4
View File
@@ -1,17 +1,14 @@
import auth from './auth';
import stories from './stories';
import configure from './configure';
import community from './community';
import moderation from './moderation';
import install from './install';
import config from './config';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import userDetail from './userDetail';
import ui from './ui';
export default {
auth,
banUserDialog,
configure,
suspendUserDialog,
@@ -20,6 +17,5 @@ export default {
community,
moderation,
install,
config,
ui,
};
@@ -24,7 +24,7 @@ export default class Configure extends Component {
render() {
const {
auth: { user },
currentUser,
canSave,
savePending,
setActiveSection,
@@ -32,7 +32,7 @@ export default class Configure extends Component {
} = this.props;
const SectionComponent = this.getSectionComponent(activeSection);
if (!can(user, 'UPDATE_CONFIG')) {
if (!can(currentUser, 'UPDATE_CONFIG')) {
return (
<p>
You must be an administrator to access config settings. Please find
@@ -87,7 +87,7 @@ export default class Configure extends Component {
Configure.propTypes = {
savePending: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
@@ -30,7 +30,7 @@ class ConfigureContainer extends Component {
return (
<Configure
auth={this.props.auth}
currentUser={this.props.currentUser}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
@@ -71,7 +71,7 @@ const withConfigureQuery = withQuery(
);
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
pending: state.configure.pending,
canSave: state.configure.canSave,
activeSection: state.configure.activeSection,
@@ -97,7 +97,7 @@ ConfigureContainer.propTypes = {
updateSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
setActiveSection: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
@@ -143,7 +143,6 @@
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
@@ -185,7 +185,7 @@ class Moderation extends Component {
commentBelongToQueue={this.props.commentBelongToQueue}
isLoadingMore={this.state.isLoadingMore}
commentCount={activeTabCount}
currentUserId={this.props.auth.user.id}
currentUserId={this.props.currentUser.id}
viewUserDetail={viewUserDetail}
selectCommentId={props.selectCommentId}
cleanUpQueue={props.cleanUpQueue}
@@ -225,7 +225,7 @@ Moderation.propTypes = {
cleanUpQueue: PropTypes.func.isRequired,
storySearchChange: PropTypes.func.isRequired,
moderation: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
queueConfig: PropTypes.object.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
handleCommentChange: PropTypes.func.isRequired,
@@ -110,7 +110,7 @@ class ModerationContainer extends Component {
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
this.props.currentUser.id === user.id
? ''
: t(
'modqueue.notify_accepted',
@@ -131,7 +131,7 @@ class ModerationContainer extends Component {
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
this.props.currentUser.id === user.id
? ''
: t(
'modqueue.notify_rejected',
@@ -152,7 +152,7 @@ class ModerationContainer extends Component {
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
this.props.currentUser.id === user.id
? ''
: t(
'modqueue.notify_reset',
@@ -515,7 +515,7 @@ const withModQueueQuery = withQuery(
const mapStateToProps = state => ({
moderation: state.moderation,
auth: state.auth,
currentUser: state.auth.user,
});
const mapDispatchToProps = dispatch => ({
@@ -1,25 +0,0 @@
import React from 'react';
import { Router, Route } from 'react-router';
import PropTypes from 'prop-types';
import Embed from './containers/Embed';
import { LoginContainer } from './containers/LoginContainer';
const routes = (
<div>
<Route exact path="/embed/stream/login" component={LoginContainer} />
<Route path="*" component={Embed} />
</div>
);
class AppRouter extends React.Component {
static contextTypes = {
history: PropTypes.object,
};
render() {
return <Router history={this.context.history} routes={routes} />;
}
}
export default AppRouter;
@@ -1,415 +0,0 @@
import jwtDecode from 'jwt-decode';
import bowser from 'bowser';
import * as actions from '../constants/auth';
import { notify } from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
import get from 'lodash/get';
export const updateStatus = status => ({
type: actions.UPDATE_STATUS,
status,
});
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
export const hideSignInDialog = () => dispatch => {
if (window.opener && window.opener !== window) {
// TODO: We need to address this when we refactor the
// login popup out of the embed.
// we are in a popup
window.close();
} else {
dispatch(checkLogin());
}
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const resetSignInDialog = () => dispatch => {
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
});
export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
export const showCreateUsernameDialog = () => ({
type: actions.SHOW_CREATEUSERNAME_DIALOG,
});
export const hideCreateUsernameDialog = () => ({
type: actions.HIDE_CREATEUSERNAME_DIALOG,
});
export const updateUsername = username => ({
type: actions.UPDATE_USERNAME,
username,
});
export const changeView = view => dispatch => {
dispatch({
type: actions.CHANGE_VIEW,
view,
});
switch (view) {
case 'SIGNUP':
window.resizeTo(500, 800);
break;
case 'FORGOT':
window.resizeTo(500, 400);
break;
default:
window.resizeTo(500, 550);
}
};
export const cleanState = () => ({
type: actions.CLEAN_STATE,
});
// Sign In Actions
const signInRequest = email => ({
type: actions.FETCH_SIGNIN_REQUEST,
email,
});
const signInFailure = error => ({
type: actions.FETCH_SIGNIN_FAILURE,
error,
});
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('token', token);
}
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
//==============================================================================
// SIGN IN
//==============================================================================
export const fetchSignIn = formData => {
return (dispatch, _, { rest }) => {
dispatch(signInRequest(formData.email));
return rest('/auth/local', { method: 'POST', body: formData })
.then(({ token }) => {
if (!bowser.safari && !bowser.ios) {
dispatch(handleAuthToken(token));
}
dispatch(hideSignInDialog());
})
.catch(error => {
console.error(error);
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(
signInFailure(t('error.email_not_verified', error.metadata))
);
} else if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch(signInFailure(t('error.email_password'), error.metadata));
} else {
dispatch(signInFailure(error));
}
});
};
};
//==============================================================================
// SIGN IN - FACEBOOK
//==============================================================================
const signInFacebookRequest = () => ({
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST,
});
const signInFacebookSuccess = user => ({
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
user,
});
const signInFacebookFailure = error => ({
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
error,
});
export const fetchSignInFacebook = () => (dispatch, _, { rest }) => {
dispatch(signInFacebookRequest());
window.open(
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
};
//==============================================================================
// SIGN UP - FACEBOOK
//==============================================================================
const signUpFacebookRequest = () => ({
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST,
});
export const fetchSignUpFacebook = () => (dispatch, _, { rest }) => {
dispatch(signUpFacebookRequest());
window.open(
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
};
export const facebookCallback = (err, data) => dispatch => {
if (err) {
dispatch(signInFacebookFailure(err));
return;
}
try {
dispatch(handleAuthToken(data.token));
dispatch(signInFacebookSuccess(data.user));
dispatch(hideSignInDialog());
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
}
};
//==============================================================================
// SIGN UP
//==============================================================================
const signUpRequest = () => ({ type: actions.FETCH_SIGNUP_REQUEST });
const signUpSuccess = user => ({ type: actions.FETCH_SIGNUP_SUCCESS, user });
const signUpFailure = error => ({ type: actions.FETCH_SIGNUP_FAILURE, error });
export const fetchSignUp = formData => (dispatch, getState, { rest }) => {
const redirectUri = getState().auth.redirectUri;
dispatch(signUpRequest());
rest('/users', {
method: 'POST',
body: formData,
headers: { 'X-Pym-Url': redirectUri },
})
.then(({ user }) => {
dispatch(signUpSuccess(user));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(signUpFailure(errorMessage));
});
};
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const fetchForgotPassword = email => (dispatch, getState, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = getState().auth.redirectUri;
rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
//==============================================================================
// LOGOUT
//==============================================================================
export const logout = () => async (
dispatch,
_,
{ rest, client, pym, localStorage }
) => {
await rest('/auth', { method: 'DELETE' });
if (localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
// Reset the websocket.
client.resetWebsocket();
dispatch({ type: actions.LOGOUT });
pym.sendMessage('coral-auth-changed');
};
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin,
});
const ErrNotLoggedIn = new Error('Not logged in');
export const checkLogin = () => (
dispatch,
_,
{ rest, client, pym, localStorage }
) => {
dispatch(checkLoginRequest());
rest('/auth')
.then(result => {
if (!result.user) {
if (localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
throw ErrNotLoggedIn;
}
// Reset the websocket.
client.resetWebsocket();
dispatch(checkLoginSuccess(result.user));
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
// This is for login via social. Usernames should be set.
if (
get(result.user, 'status.username.status') === 'UNSET' &&
!get(result.user, 'status.banned.status')
) {
dispatch(showCreateUsernameDialog());
}
})
.catch(error => {
if (error !== ErrNotLoggedIn) {
console.error(error);
}
if (error.status && error.status === 401 && localStorage) {
// Unauthorized.
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
export const validForm = () => ({ type: actions.VALID_FORM });
export const invalidForm = error => ({ type: actions.INVALID_FORM, error });
//==============================================================================
// VERIFY EMAIL
//==============================================================================
const verifyEmailRequest = () => ({
type: actions.VERIFY_EMAIL_REQUEST,
});
const verifyEmailSuccess = () => ({
type: actions.VERIFY_EMAIL_SUCCESS,
});
const verifyEmailFailure = error => ({
type: actions.VERIFY_EMAIL_FAILURE,
error,
});
export const requestConfirmEmail = email => (dispatch, getState, { rest }) => {
const redirectUri = getState().auth.redirectUri;
dispatch(verifyEmailRequest());
return rest('/users/resend-verify', {
method: 'POST',
body: { email },
headers: { 'X-Pym-Url': redirectUri },
})
.then(() => {
dispatch(verifyEmailSuccess());
})
.catch(error => {
console.error(error);
dispatch(verifyEmailFailure(error));
throw error;
});
};
// Login Popup actions.
export const setRequireEmailVerification = required => ({
type: actions.SET_REQUIRE_EMAIL_VERIFICATION,
required,
});
export const setRedirectUri = uri => ({
type: actions.SET_REDIRECT_URI,
uri,
});
//==============================================================================
// Edit Username
//==============================================================================
const editUsernameFailure = error => ({
type: actions.EDIT_USERNAME_FAILURE,
error,
});
const editUsernameSuccess = () => ({ type: actions.EDIT_USERNAME_SUCCESS });
export const editName = username => (dispatch, _, { rest }) => {
return rest('/account/username', { method: 'PUT', body: { username } })
.then(() => {
dispatch(editUsernameSuccess());
dispatch(notify('success', t('framework.success_name_update')));
})
.catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(editUsernameFailure(errorMessage));
});
};
@@ -0,0 +1,19 @@
import * as actions from '../constants/login';
import { checkLogin } from 'coral-framework/actions/auth';
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
export const hideSignInDialog = () => dispatch => {
dispatch(checkLogin());
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
});
export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
@@ -16,17 +16,10 @@ import cn from 'classnames';
export default class Embed extends React.Component {
changeTab = tab => {
// TODO: move data fetching to appropiate containers.
switch (tab) {
case 'profile':
this.props.data.refetch();
break;
}
this.props.setActiveTab(tab);
};
getTabs() {
const { user } = this.props.auth;
const tabs = [
<Tab
key="stream"
@@ -43,7 +36,7 @@ export default class Embed extends React.Component {
{t('framework.my_profile')}
</Tab>,
];
if (can(user, 'UPDATE_ASSET_CONFIG')) {
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
tabs.push(
<Tab
key="config"
@@ -64,11 +57,12 @@ export default class Embed extends React.Component {
root,
root: { asset },
data,
auth: { showSignInDialog, signInDialogFocus },
showSignInDialog,
signInDialogFocus,
blurSignInDialog,
focusSignInDialog,
hideSignInDialog,
router: { location: { query: { parentUrl } } },
parentUrl,
} = this.props;
const hasHighlightedComment = !!commentId;
@@ -81,9 +75,7 @@ export default class Embed extends React.Component {
<AutomaticAssetClosure asset={asset} />
<IfSlotIsNotEmpty slot="login">
<Popup
href={`embed/stream/login?parentUrl=${encodeURIComponent(
parentUrl
)}`}
href={`login?parentUrl=${encodeURIComponent(parentUrl)}`}
title="Login"
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
open={showSignInDialog}
@@ -138,11 +130,13 @@ export default class Embed extends React.Component {
Embed.propTypes = {
setActiveTab: PropTypes.func,
auth: PropTypes.object,
currentUser: PropTypes.object,
showSignInDialog: PropTypes.bool,
signInDialogFocus: PropTypes.bool,
blurSignInDialog: PropTypes.func,
focusSignInDialog: PropTypes.func,
hideSignInDialog: PropTypes.func,
router: PropTypes.object,
parentUrl: PropTypes.string,
commentId: PropTypes.string,
root: PropTypes.object,
activeTab: PropTypes.string,
@@ -1,57 +0,0 @@
export const CHANGE_VIEW = 'CHANGE_VIEW';
export const CLEAN_STATE = 'CLEAN_STATE';
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
export const CREATE_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
export const CREATE_USERNAME = 'CREATE_USERNAME';
export const SHOW_CREATEUSERNAME_DIALOG = 'SHOW_CREATEUSERNAME_DIALOG';
export const HIDE_CREATEUSERNAME_DIALOG = 'HIDE_CREATEUSERNAME_DIALOG';
export const EDIT_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
export const EDIT_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
export const EDIT_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
export const EDIT_USERNAME = 'CREATE_USERNAME';
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
export const FETCH_SIGNIN_REQUEST = 'FETCH_SIGNIN_REQUEST';
export const FETCH_SIGNIN_FAILURE = 'FETCH_SIGNIN_FAILURE';
export const FETCH_SIGNIN_SUCCESS = 'FETCH_SIGNIN_SUCCESS';
export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST';
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
export const LOGOUT = 'LOGOUT';
export const INVALID_FORM = 'INVALID_FORM';
export const VALID_FORM = 'VALID_FORM';
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
export const UPDATE_USERNAME = 'UPDATE_USERNAME';
// Login Popup actions.
export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
export const UPDATE_STATUS = 'UPDATE_STATUS';
@@ -0,0 +1,4 @@
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
@@ -1,14 +1,27 @@
import defaultTo from 'lodash/defaultTo';
const prefix = 'TALK_EMBED_STREAM';
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
);
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
);
export const REPLY_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_REPLY_COMMENTS_LOAD_DEPTH, '3')
);
export const THREADING_LEVEL = parseInt(
defaultTo(process.env.TALK_THREADING_LEVEL, '3')
);
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
@@ -8,8 +8,13 @@ import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
import { Spinner } from 'coral-ui';
import * as authActions from '../actions/auth';
import * as assetActions from '../actions/asset';
import {
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
} from '../actions/login';
import { updateStatus } from 'coral-framework/actions/auth';
import { fetchAssetSuccess } from '../actions/asset';
import {
getDefinitionName,
getSlotFragmentSpreads,
@@ -24,16 +29,6 @@ import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
import { setActiveTab } from '../actions/embed';
const {
logout,
checkLogin,
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
updateStatus,
} = authActions;
const { fetchAssetSuccess } = assetActions;
class EmbedContainer extends React.Component {
static contextTypes = {
pym: PropTypes.object,
@@ -42,7 +37,7 @@ class EmbedContainer extends React.Component {
subscriptions = [];
subscribeToUpdates(props = this.props) {
if (props.auth.loggedIn) {
if (props.currentUser) {
const newSubscriptions = [
{
document: USER_BANNED_SUBSCRIPTION,
@@ -80,7 +75,7 @@ class EmbedContainer extends React.Component {
props.data.subscribeToMore({
document: s.document,
variables: {
user_id: props.auth.user.id,
user_id: props.currentUser.id,
},
updateQuery: s.updateQuery,
})
@@ -107,7 +102,7 @@ class EmbedContainer extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
if (this.props.currentUser !== nextProps.currentUser) {
// Refetch after login/logout.
this.props.data.refetch();
this.resubscribe(nextProps);
@@ -138,7 +133,23 @@ class EmbedContainer extends React.Component {
if (!this.props.root.asset) {
return <Spinner />;
}
return <Embed {...this.props} />;
return (
<Embed
setActiveTab={this.props.setActiveTab}
currentUser={this.props.currentUser}
blurSignInDialog={this.props.blurSignInDialog}
focusSignInDialog={this.props.focusSignInDialog}
hideSignInDialog={this.props.hideSignInDialog}
router={this.props.router}
commentId={this.props.commentId}
root={this.props.root}
activeTab={this.props.activeTab}
data={this.props.data}
showSignInDialog={this.props.showSignInDialog}
signInDialogFocus={this.props.signInDialogFocus}
parentUrl={this.props.parentUrl}
/>
);
}
}
@@ -255,21 +266,46 @@ const EMBED_QUERY = gql`
`;
export const withEmbedQuery = withQuery(EMBED_QUERY, {
options: ({ auth, commentId, assetId, assetUrl, sortBy, sortOrder }) => ({
options: ({
currentUser,
commentId,
assetId,
assetUrl,
sortBy,
sortOrder,
}) => ({
variables: {
assetId,
assetUrl,
commentId,
hasComment: commentId !== '',
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
excludeIgnored: Boolean(currentUser && currentUser.id),
sortBy,
sortOrder,
},
}),
});
EmbedContainer.propTypes = {
setActiveTab: PropTypes.func,
currentUser: PropTypes.object,
blurSignInDialog: PropTypes.func,
focusSignInDialog: PropTypes.func,
hideSignInDialog: PropTypes.func,
router: PropTypes.object,
commentId: PropTypes.string,
root: PropTypes.object,
activeTab: PropTypes.string,
parentUrl: PropTypes.string,
data: PropTypes.object,
fetchAssetSuccess: PropTypes.func,
showSignInDialog: PropTypes.bool,
signInDialogFocus: PropTypes.bool,
};
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
checkedInitialLogin: state.auth.checkedInitialLogin,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
@@ -277,13 +313,14 @@ const mapStateToProps = state => ({
config: state.config,
sortOrder: state.stream.sortOrder,
sortBy: state.stream.sortBy,
showSignInDialog: state.login.showSignInDialog,
signInDialogFocus: state.login.signInDialogFocus,
parentUrl: state.login.parentUrl,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
logout,
checkLogin,
setActiveTab,
fetchAssetSuccess,
notify,
@@ -297,6 +334,6 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(props => !props.auth.checkedInitialLogin, renderComponent(Spinner)),
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
withEmbedQuery
)(EmbedContainer);
@@ -1,4 +0,0 @@
import React from 'react';
import Slot from 'coral-framework/components/Slot';
export const LoginContainer = () => <Slot fill="login" />;
+2 -39
View File
@@ -1,59 +1,22 @@
import React from 'react';
import { render } from 'react-dom';
import Embed from './containers/Embed';
import {
checkLogin,
handleAuthToken,
logout,
} from 'coral-embed-stream/src/actions/auth';
import graphqlExtension from './graphql';
import { addExternalConfig } from 'coral-embed-stream/src/actions/config';
import { createContext } from 'coral-framework/services/bootstrap';
import AppRouter from './AppRouter';
import reducers from './reducers';
import TalkProvider from 'coral-framework/components/TalkProvider';
import pluginsConfig from 'pluginsConfig';
// TODO: move init code into `bootstrap` service after auth has been refactored.
function preInit({ store, pym, inIframe }) {
// TODO: This is popup specific code and needs to be refactored.
if (!inIframe) {
store.dispatch(addExternalConfig({}));
store.dispatch(checkLogin());
return;
}
pym.onMessage('login', token => {
if (token) {
store.dispatch(handleAuthToken(token));
}
store.dispatch(checkLogin());
});
pym.onMessage('logout', () => {
store.dispatch(logout());
});
return new Promise(resolve => {
pym.sendMessage('getConfig');
pym.onMessage('config', config => {
store.dispatch(addExternalConfig(JSON.parse(config)));
store.dispatch(checkLogin());
resolve();
});
});
}
async function main() {
const context = await createContext({
reducers,
graphqlExtension,
pluginsConfig,
preInit,
});
render(
<TalkProvider {...context}>
<AppRouter />
<Embed />
</TalkProvider>,
document.querySelector('#talk-embed-stream-container')
);
@@ -1,249 +0,0 @@
import * as actions from '../constants/auth';
import pym from 'coral-framework/services/pym';
import merge from 'lodash/merge';
const initialState = {
isLoading: false,
loggedIn: false,
user: null,
showSignInDialog: false,
signInDialogFocus: false,
showCreateUsernameDialog: false,
checkedInitialLogin: false,
view: 'SIGNIN',
error: null,
passwordRequestSuccess: null,
passwordRequestFailure: null,
emailVerificationFailure: false,
emailVerificationLoading: false,
emailVerificationSuccess: false,
successSignUp: false,
fromSignUp: false,
requireEmailConfirmation: false,
redirectUri: pym.parentUrl || location.href,
};
const purge = user => {
const {settings, ...userData} = user; // eslint-disable-line
return userData;
};
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.FOCUS_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: true,
};
case actions.BLUR_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: false,
};
case actions.SHOW_SIGNIN_DIALOG:
return {
...state,
showSignInDialog: true,
signInDialogFocus: true,
};
case actions.RESET_SIGNIN_DIALOG:
case actions.HIDE_SIGNIN_DIALOG:
return {
...state,
isLoading: false,
showSignInDialog: false,
signInDialogFocus: false,
view: 'SIGNIN',
error: null,
passwordRequestFailure: null,
passwordRequestSuccess: null,
emailVerificationFailure: false,
emailVerificationSuccess: false,
emailVerificationLoading: false,
successSignUp: false,
};
case actions.SHOW_CREATEUSERNAME_DIALOG:
return {
...state,
showCreateUsernameDialog: true,
};
case actions.HIDE_CREATEUSERNAME_DIALOG:
return {
...state,
showCreateUsernameDialog: false,
};
case actions.CREATE_USERNAME_SUCCESS:
return {
...state,
showCreateUsernameDialog: false,
error: null,
};
case actions.CREATE_USERNAME_FAILURE:
return {
...state,
error: action.error,
};
case actions.CHANGE_VIEW:
return {
...state,
error: action.error,
view: action.view,
};
case actions.CLEAN_STATE:
return initialState;
case actions.FETCH_SIGNIN_REQUEST:
return {
...state,
email: action.email,
isLoading: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
checkedInitialLogin: true,
loggedIn: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
checkedInitialLogin: true,
loggedIn: true,
user: purge(action.user),
};
case actions.FETCH_SIGNIN_SUCCESS:
return {
...state,
loggedIn: true,
user: purge(action.user),
};
case actions.FETCH_SIGNIN_FAILURE:
return {
...state,
isLoading: false,
error: action.error,
user: null,
view:
action.error.translation_key === 'EMAIL_NOT_VERIFIED'
? 'RESEND_VERIFICATION'
: state.view,
};
case actions.FETCH_SIGNUP_FACEBOOK_REQUEST:
return {
...state,
fromSignUp: true,
};
case actions.FETCH_SIGNIN_FACEBOOK_REQUEST:
return {
...state,
fromSignUp: false,
};
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return {
...state,
loggedIn: true,
user: purge(action.user),
};
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
return {
...state,
error: action.error,
user: null,
};
case actions.FETCH_SIGNUP_REQUEST:
return {
...state,
isLoading: true,
};
case actions.FETCH_SIGNUP_FAILURE:
return {
...state,
error: action.error,
isLoading: false,
};
case actions.FETCH_SIGNUP_SUCCESS:
return {
...state,
isLoading: false,
successSignUp: true,
};
case actions.LOGOUT:
return {
...state,
user: null,
isLoading: false,
loggedIn: false,
};
case actions.INVALID_FORM:
return {
...state,
error: action.error,
};
case actions.VALID_FORM:
return {
...state,
error: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestFailure: null,
passwordRequestSuccess:
'If you have a registered account, a password reset link was sent to that email',
};
case actions.FETCH_FORGOT_PASSWORD_FAILURE:
return {
...state,
passwordRequestFailure:
'There was an error sending your password reset email. Please try again soon!',
passwordRequestSuccess: null,
};
case actions.UPDATE_USERNAME:
return {
...state,
user: {
...state.user,
username: action.username,
lowercaseUsername: action.username.toLowerCase(),
},
};
case actions.VERIFY_EMAIL_FAILURE:
return {
...state,
emailVerificationFailure: action.error,
emailVerificationLoading: false,
};
case actions.VERIFY_EMAIL_REQUEST:
return {
...state,
emailVerificationLoading: true,
};
case actions.VERIFY_EMAIL_SUCCESS:
return {
...state,
emailVerificationSuccess: true,
emailVerificationLoading: false,
};
case actions.SET_REQUIRE_EMAIL_VERIFICATION:
return {
...state,
requireEmailConfirmation: action.required,
};
case actions.SET_REDIRECT_URI:
return {
...state,
redirectUri: action.uri,
};
case actions.UPDATE_STATUS: {
return {
...state,
user: {
...state.user,
status: merge({}, state.user.status, action.status),
},
};
}
default:
return state;
}
}
@@ -1,15 +1,13 @@
import auth from './auth';
import login from './login';
import asset from './asset';
import embed from './embed';
import config from './config';
import configure from './configure';
import stream from './stream';
export default {
auth,
login,
asset,
embed,
config,
configure,
stream,
};
@@ -0,0 +1,38 @@
import * as actions from '../constants/login';
import pym from 'coral-framework/services/pym';
const initialState = {
parentUrl: pym.parentUrl || location.href,
showSignInDialog: false,
signInDialogFocus: false,
};
export default function login(state = initialState, action) {
switch (action.type) {
case actions.FOCUS_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: true,
};
case actions.BLUR_SIGNIN_DIALOG:
return {
...state,
signInDialogFocus: false,
};
case actions.SHOW_SIGNIN_DIALOG:
return {
...state,
showSignInDialog: true,
signInDialogFocus: true,
};
case actions.HIDE_SIGNIN_DIALOG:
return {
...state,
showSignInDialog: false,
signInDialogFocus: false,
};
default:
return state;
}
}
@@ -1,5 +1,5 @@
import * as actions from '../constants/stream';
import * as authActions from '../constants/auth';
import * as authActions from 'coral-framework/constants/auth';
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
@@ -10,12 +10,7 @@ import NotLoggedIn from '../components/NotLoggedIn';
import { Spinner } from 'coral-ui';
import CommentHistory from '../components/CommentHistory';
// TODO: Auth logic needs refactoring.
import {
showSignInDialog,
checkLogin,
} from 'coral-embed-stream/src/actions/auth';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
@@ -24,7 +19,7 @@ import t from 'coral-framework/services/i18n';
class ProfileContainer extends Component {
componentWillReceiveProps(nextProps) {
if (!this.props.auth.loggedIn && nextProps.auth.loggedIn) {
if (!this.props.currentUser && nextProps.currentUser) {
// Refetch after login.
this.props.data.refetch();
}
@@ -55,13 +50,7 @@ class ProfileContainer extends Component {
};
render() {
const {
auth,
auth: { user: authUser },
showSignInDialog,
root,
data,
} = this.props;
const { currentUser, showSignInDialog, root, data } = this.props;
const { me } = this.props.root;
const loading = this.props.data.loading;
@@ -69,7 +58,7 @@ class ProfileContainer extends Component {
return <div>{this.props.data.error.message}</div>;
}
if (!auth.loggedIn) {
if (!currentUser) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
@@ -77,7 +66,7 @@ class ProfileContainer extends Component {
return <Spinner />;
}
const localProfile = authUser.profiles.find(p => p.provider === 'local');
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
return (
@@ -168,15 +157,20 @@ const withProfileQuery = withQuery(
${getSlotFragmentSpreads(slots, 'root')}
}
${CommentFragment}
`
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog, checkLogin }, dispatch);
bindActionCreators({ showSignInDialog }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -59,7 +59,7 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
loadNewReplies,
auth: { user },
currentUser,
emit,
viewAllComments,
} = this.props;
@@ -111,7 +111,7 @@ class Stream extends React.Component {
disableReply={!open}
postComment={postComment}
asset={asset}
currentUser={user}
currentUser={currentUser}
highlighted={comment.id}
postFlag={postFlag}
postDontAgree={postDontAgree}
@@ -150,7 +150,7 @@ class Stream extends React.Component {
setActiveStreamTab,
loadNewReplies,
loadMoreComments,
auth: { user },
currentUser,
emit,
sortOrder,
sortBy,
@@ -200,7 +200,7 @@ class Stream extends React.Component {
notify={notify}
disableReply={asset.isClosed}
postComment={postComment}
currentUser={user}
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
loadMore={loadMoreComments}
@@ -230,21 +230,23 @@ class Stream extends React.Component {
postComment,
notify,
updateItem,
auth: { loggedIn, user },
currentUser,
} = this.props;
const { keepCommentBox } = this.state;
const open = !asset.isClosed;
const banned = get(user, 'status.banned.status');
const suspensionUntil = get(user, 'status.suspension.until');
const rejectedUsername = get(user, 'status.username.status') === 'REJECTED';
const changedUsername = get(user, 'status.username.status') === 'CHANGED';
const banned = get(currentUser, 'status.banned.status');
const suspensionUntil = get(currentUser, 'status.suspension.until');
const rejectedUsername =
get(currentUser, 'status.username.status') === 'REJECTED';
const changedUsername =
get(currentUser, 'status.username.status') === 'CHANGED';
const temporarilySuspended =
user && suspensionUntil && new Date(suspensionUntil) > new Date();
currentUser && suspensionUntil && new Date(suspensionUntil) > new Date();
const showCommentBox =
loggedIn &&
currentUser &&
((!banned &&
!temporarilySuspended &&
!rejectedUsername &&
@@ -289,7 +291,8 @@ class Stream extends React.Component {
</RestrictedMessageBox>
)}
{changedUsername && <ChangedUsername />}
{!banned && rejectedUsername && <ChangeUsername user={user} />}
{!banned &&
rejectedUsername && <ChangeUsername user={currentUser} />}
{banned && <BannedAccount />}
{showCommentBox && (
<CommentBox
@@ -300,7 +303,7 @@ class Stream extends React.Component {
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={user}
currentUser={currentUser}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
/>
@@ -312,10 +315,10 @@ class Stream extends React.Component {
<Slot fill="stream" queryData={slotQueryData} {...slotProps} />
{loggedIn && (
{currentUser && (
<ModerationLink
assetId={asset.id}
isAdmin={can(user, 'MODERATE_COMMENTS')}
isAdmin={can(currentUser, 'MODERATE_COMMENTS')}
/>
)}
@@ -342,12 +345,11 @@ Stream.propTypes = {
deleteAction: PropTypes.func,
showSignInDialog: PropTypes.func,
loadNewReplies: PropTypes.func,
auth: PropTypes.object,
currentUser: PropTypes.object,
emit: PropTypes.func,
sortOrder: PropTypes.string,
sortBy: PropTypes.string,
loading: PropTypes.bool,
editName: PropTypes.func,
appendItemArray: PropTypes.func,
updateItem: PropTypes.func,
viewAllComments: PropTypes.func,
@@ -4,7 +4,10 @@ import Comment from '../components/Comment';
import { withFragments } from 'coral-framework/hocs';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { withSetCommentStatus } from 'coral-framework/graphql/mutations';
import { THREADING_LEVEL } from '../../../constants/stream';
import {
THREADING_LEVEL,
REPLY_COMMENTS_LOAD_DEPTH,
} from '../../../constants/stream';
import hoistStatics from 'recompose/hoistStatics';
import { nest } from '../../../graphql/utils';
@@ -118,7 +121,7 @@ const withCommentFragments = withFragments({
...CoralEmbedStream_Comment_SingleComment
${nest(
`
replies(query: {limit: 3, excludeIgnored: $excludeIgnored}) {
replies(query: {limit: ${REPLY_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored}) {
nodes {
...CoralEmbedStream_Comment_SingleComment
...nest
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gql, compose } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
ADDTL_COMMENTS_ON_LOAD_MORE,
ASSET_COMMENTS_LOAD_DEPTH,
THREADING_LEVEL,
} from '../../../constants/stream';
import {
@@ -14,8 +16,8 @@ import {
withEditComment,
} from 'coral-framework/graphql/mutations';
import * as authActions from 'coral-embed-stream/src/actions/auth';
import * as notificationActions from 'coral-framework/actions/notification';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { notify } from 'coral-framework/actions/notification';
import {
setActiveReplyBox,
setActiveTab,
@@ -40,9 +42,6 @@ import {
} from '../../../graphql/utils';
import StreamError from '../components/StreamError';
const { showSignInDialog, editName } = authActions;
const { notify } = notificationActions;
class StreamContainer extends React.Component {
commentsAddedSubscription = null;
commentsEditedSubscription = null;
@@ -60,8 +59,8 @@ class StreamContainer extends React.Component {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (
this.props.auth.user &&
commentEdited.user.id === this.props.auth.user.id
this.props.currentUser &&
commentEdited.user.id === this.props.currentUser.id
) {
return prev;
}
@@ -92,8 +91,8 @@ class StreamContainer extends React.Component {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (
this.props.auth.user &&
commentAdded.user.id === this.props.auth.user.id
this.props.currentUser &&
commentAdded.user.id === this.props.currentUser.id
) {
return prev;
}
@@ -204,8 +203,8 @@ class StreamContainer extends React.Component {
}
}
userIsDegraged({ auth: { user } } = this.props) {
return !can(user, 'INTERACT_WITH_COMMUNITY');
userIsDegraged({ currentUser } = this.props) {
return !can(currentUser, 'INTERACT_WITH_COMMUNITY');
}
render() {
@@ -225,8 +224,28 @@ class StreamContainer extends React.Component {
return (
<Stream
{...this.props}
loadMore={this.loadMore}
asset={this.props.asset}
activeStreamTab={this.props.activeStreamTab}
data={this.props.data}
root={this.props.root}
activeReplyBox={this.props.activeReplyBox}
setActiveReplyBox={this.props.setActiveReplyBox}
commentClassNames={this.props.commentClassNames}
setActiveStreamTab={this.props.setActiveStreamTab}
postFlag={this.props.postFlag}
postDontAgree={this.props.postDontAgree}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
currentUser={this.props.currentUser}
emit={this.props.emit}
sortOrder={this.props.sortOrder}
sortBy={this.props.sortBy}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
viewAllComments={this.props.viewAllComments}
notify={this.props.notify}
postComment={this.props.postComment}
editComment={this.props.editComment}
loadMoreComments={this.loadMoreComments}
loadNewReplies={this.loadNewReplies}
userIsDegraged={this.userIsDegraged()}
@@ -236,6 +255,33 @@ class StreamContainer extends React.Component {
}
}
StreamContainer.propTypes = {
asset: PropTypes.object,
activeStreamTab: PropTypes.string,
data: PropTypes.object,
root: PropTypes.object,
activeReplyBox: PropTypes.string,
setActiveReplyBox: PropTypes.func,
commentClassNames: PropTypes.array,
setActiveStreamTab: PropTypes.func,
postFlag: PropTypes.func,
postDontAgree: PropTypes.func,
deleteAction: PropTypes.func,
showSignInDialog: PropTypes.func,
currentUser: PropTypes.object,
emit: PropTypes.func,
sortOrder: PropTypes.string,
sortBy: PropTypes.string,
loading: PropTypes.bool,
appendItemArray: PropTypes.func,
updateItem: PropTypes.func,
viewAllComments: PropTypes.func,
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
editComment: PropTypes.func,
previousTab: PropTypes.string,
};
const commentFragment = gql`
fragment CoralEmbedStream_Stream_comment on Comment {
id
@@ -379,7 +425,7 @@ const fragments = {
requireEmailConfirmation
}
totalCommentCount @skip(if: $hasComment)
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
comments(query: {limit: ${ASSET_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
nodes {
...CoralEmbedStream_Stream_comment
}
@@ -396,7 +442,7 @@ const fragments = {
};
const mapStateToProps = state => ({
auth: state.auth,
currentUser: state.auth.user,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
@@ -417,7 +463,6 @@ const mapDispatchToProps = dispatch =>
showSignInDialog,
notify,
setActiveReplyBox,
editName,
viewAllComments,
setActiveStreamTab: setActiveTab,
},
@@ -72,17 +72,6 @@ body {
font-weight: bold;
}
/* Coral sign in button */
#coralSignInButton {
background-color: #2a2a2a;
color: #FFF;
}
#coralSignInButton:hover {
background-color: #767676;
}
/* Info Box Styles */
+110
View File
@@ -0,0 +1,110 @@
import * as actions from '../constants/auth';
import jwtDecode from 'jwt-decode';
function cleanAuthData(localStorage) {
localStorage.removeItem('token');
localStorage.removeItem('exp');
}
export const checkLogin = () => (
dispatch,
_,
{ rest, client, pym, localStorage }
) => {
dispatch(checkLoginRequest());
rest('/auth')
.then(result => {
if (!result.user) {
if (localStorage) {
cleanAuthData(localStorage);
}
dispatch(checkLoginSuccess(null));
return;
}
// Reset the websocket.
client.resetWebsocket();
dispatch(checkLoginSuccess(result.user));
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
})
.catch(error => {
if (error.status && error.status === 401 && localStorage) {
// Unauthorized.
cleanAuthData(localStorage);
} else {
console.error(error);
}
dispatch(checkLoginFailure(error));
});
};
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error,
});
const checkLoginSuccess = user => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
});
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('token', token);
}
dispatch(checkLogin());
};
export const handleSuccessfulLogin = (user, token) => (
dispatch,
_,
{ client, localStorage }
) => {
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('token', token);
}
client.resetWebsocket();
dispatch({
type: actions.HANDLE_SUCCESSFUL_LOGIN,
user,
});
};
/**
* Logout
*/
export const logout = () => async (
dispatch,
_,
{ rest, client, pym, localStorage }
) => {
await rest('/auth', { method: 'DELETE' });
if (localStorage) {
cleanAuthData(localStorage);
}
// Reset the websocket.
client.resetWebsocket();
dispatch({ type: actions.LOGOUT });
pym.sendMessage('coral-auth-changed');
};
export const updateStatus = status => ({
type: actions.UPDATE_STATUS,
status,
});
export const updateUsername = username => ({
type: actions.UPDATE_USERNAME,
username,
});
+6
View File
@@ -0,0 +1,6 @@
import { MERGE_CONFIG } from '../constants/config';
export const mergeConfig = config => ({
type: MERGE_CONFIG,
config,
});
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactRecaptcha from 'react-recaptcha';
class Recaptcha extends React.Component {
static contextTypes = {
store: PropTypes.object,
};
ref = null;
handleRef = ref => {
this.ref = ref;
};
reset = () => this.ref.reset();
getSiteKey() {
// This should be fine because it's static and will never change.
// Prefer this to connect HOC because wie expose the instance method
// `reset`
return this.context.store.getState().config.static.TALK_RECAPTCHA_PUBLIC;
}
render() {
return (
<ReactRecaptcha
ref={this.handleRef}
sitekey={this.getSiteKey()}
render={this.props.render}
theme={this.props.theme}
onloadCallback={this.props.onLoad}
verifyCallback={this.props.onVerify}
size={this.props.size}
className={this.props.className}
/>
);
}
}
Recaptcha.defaultProps = {
render: 'explicit',
theme: 'light',
size: 'normal',
};
Recaptcha.propTypes = {
onLoad: PropTypes.func,
onVerify: PropTypes.func.isRequired,
theme: PropTypes.string,
render: PropTypes.string,
size: PropTypes.string,
className: PropTypes.string,
};
export default Recaptcha;
+2 -1
View File
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
import { getShallowChanges } from 'coral-framework/utils';
const emptyConfig = {};
@@ -68,7 +69,7 @@ class Slot extends React.Component {
} = this.props;
const { plugins } = this.context;
let children = this.getChildren();
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
const pluginConfig = get(reduxState, 'config.pluginConfig') || emptyConfig;
if (children.length === 0 && DefaultComponent) {
const props = plugins.getSlotComponentProps(
DefaultComponent,
+11
View File
@@ -0,0 +1,11 @@
const prefix = `TALK_FRAMEWORK`;
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
export const LOGOUT = `${prefix}_LOGOUT`;
export const HANDLE_SUCCESSFUL_LOGIN = `${prefix}_HANDLE_SUCCESSFUL_LOGIN`;
export const UPDATE_STATUS = '${prefix}_UPDATE_STATUS';
export const UPDATE_USERNAME = '${prefix}_UPDATE_USERNAME';
@@ -0,0 +1,3 @@
const prefix = `TALK_FRAMEWORK`;
export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`;
+7
View File
@@ -6,3 +6,10 @@ export { default as withEmit } from './withEmit';
export { default as excludeIf } from './excludeIf';
export { default as connect } from './connect';
export { default as withMergedSettings } from './withMergedSettings';
export { default as withSignIn } from './withSignIn';
export { default as withSignUp } from './withSignUp';
export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
@@ -0,0 +1,68 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { translateError } from '../utils';
/**
* WithForgotPassword provides properties
* `forgotPasssword`,
* `loading`,
* `errorMessage`,
* `success`.
*/
export default hoistStatics(WrappedComponent => {
class WithForgotPassword extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
pym: PropTypes.object,
};
state = {
error: null,
loading: false,
success: false,
};
forgotPassword = (email, redirectUri) => {
if (!redirectUri) {
redirectUri = this.context.pym.parentUrl || location.href;
}
const { rest } = this.context;
this.setState({ loading: true, error: null, success: false });
rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => {
this.setState({ loading: false, error: null, success: true });
})
.catch(error => {
console.error(error);
this.setState({ loading: false, error });
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
forgotPassword={this.forgotPassword}
success={this.state.success}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
/>
);
}
}
return WithForgotPassword;
});
@@ -0,0 +1,69 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { translateError } from '../utils';
/**
* WithResendEmailConfirmaton provides properties
* `resendEmailConfirmation`,
* `loading`,
* `errorMessage`,
* `success`.
*/
export default hoistStatics(WrappedComponent => {
class WithResendEmailConfirmaton extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
pym: PropTypes.object,
};
state = {
error: null,
loading: false,
success: false,
};
resendEmailConfirmation = (email, redirectUri) => {
if (!redirectUri) {
redirectUri = this.context.pym.parentUrl || location.href;
}
const { rest } = this.context;
this.setState({ loading: true, error: null, success: false });
rest('/users/resend-verify', {
method: 'POST',
body: { email },
headers: { 'X-Pym-Url': redirectUri },
})
.then(() => {
this.setState({ loading: false, error: null, success: true });
})
.catch(error => {
console.error(error);
this.setState({ loading: false, error });
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
resendEmailConfirmation={this.resendEmailConfirmation}
success={this.state.success}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
/>
);
}
}
return WithResendEmailConfirmaton;
});
@@ -0,0 +1,100 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { getErrorMessages } from '../utils';
import validate from '../helpers/validate';
import errorMsg from 'coral-framework/helpers/error';
import t from '../services/i18n';
import { withSetUsername as withSetUsernameMutation } from 'coral-framework/graphql/mutations';
import { updateUsername, updateStatus } from '../actions/auth';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import get from 'lodash/get';
/**
* withSetUsername provides properties
* `setUsername`,
* `loading`,
* `errorMessage`,
* `requireEmailVerification`,
* `success`,
* `validateUsername`.
*/
const withSetUsername = hoistStatics(WrappedComponent => {
class WithSetUsername extends React.Component {
static propTypes = {
setUsername: PropTypes.func.isRequired,
currentUserId: PropTypes.string,
updateUsername: PropTypes.func.isRequired,
updateStatus: PropTypes.func.isRequired,
};
state = {
error: null,
loading: false,
success: false,
};
validateUsername = value => {
if (!value) {
return t('error.required_field');
}
return validate.username(value) ? null : errorMsg.username;
};
setUsername = async username => {
if (!this.props.currentUserId) {
throw new Error('User not logged in');
}
try {
await this.props.setUsername(this.props.currentUserId, username);
this.props.updateUsername(username);
this.props.updateStatus({ username: { status: 'SET' } });
this.setState({ success: true, loading: false, error: null });
} catch (error) {
if (!error.status || error.status !== 401) {
console.error(error);
}
const changeSet = { success: false, loading: false, error };
this.setState(changeSet);
}
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return getErrorMessages(this.state.error).join(', ');
}
render() {
return (
<WrappedComponent
{...this.props}
setUsername={this.setUsername}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
success={this.state.success}
validateUsername={this.validateUsername}
/>
);
}
}
return WithSetUsername;
});
const mapStateToProps = ({ auth }) => ({
currentUserId: get(auth, 'user.id'),
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateUsername, updateStatus }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withSetUsernameMutation,
withSetUsername
);
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { handleSuccessfulLogin } from '../actions/auth';
import { translateError } from '../utils';
import { t } from '../services/i18n';
/**
* WithSignIn provides properties
* `signIn`
* `loading`
* `errorMessage`
* `requireRecaptcha`
* `requireEmailConfirmation`
* 'success'
*/
export default hoistStatics(WrappedComponent => {
class WithSignIn extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
};
state = {
error: null,
loading: false,
success: false,
requireRecaptcha: false,
requireEmailConfirmation: false,
};
signIn = (email, password, recaptchaResponse) => {
const { store, rest } = this.context;
const params = {
method: 'POST',
body: {
email,
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse,
};
}
rest('/auth/local', params)
.then(({ user, token }) => {
this.setState({ success: true, loading: false, error: null });
store.dispatch(handleSuccessfulLogin(user, token));
})
.catch(error => {
if (!error.status || error.status !== 401) {
console.error(error);
}
const changeSet = { success: false, loading: false, error };
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
changeSet.requireRecaptcha = !!this.context.store.getState().config
.static.TALK_RECAPTCHA_PUBLIC;
} else if (error.translation_key === 'EMAIL_NOT_VERIFIED') {
changeSet.requireEmailConfirmation = true;
}
this.setState(changeSet);
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return this.state.error.translation_key === 'NOT_AUTHORIZED'
? t('error.email_password')
: translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
signIn={this.signIn}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
requireRecaptcha={this.state.requireRecaptcha}
requireEmailConfirmation={this.state.requireEmailConfirmation}
success={this.state.success}
/>
);
}
}
return WithSignIn;
});
+124
View File
@@ -0,0 +1,124 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import { compose, gql } from 'react-apollo';
import PropTypes from 'prop-types';
import { translateError } from '../utils';
import validate from '../helpers/validate';
import errorMsg from 'coral-framework/helpers/error';
import t from '../services/i18n';
import withQuery from './withQuery';
import get from 'lodash/get';
const requiredFields = ['username', 'email', 'password'];
const allFields = requiredFields;
const QUERY = gql`
query TalkFramework_WithSignUpQuery {
settings {
requireEmailConfirmation
}
}
`;
export const withSettingsQuery = withQuery(QUERY);
/**
* withSignUp provides properties
* `signUp`,
* `loading`,
* `errorMessage`,
* `requireEmailVerification`,
* `success`,
* `validate`.
*/
const withSignUp = hoistStatics(WrappedComponent => {
class WithSignUp extends React.Component {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
pym: PropTypes.object,
};
static propTypes = {
root: PropTypes.object.isRequired,
};
state = {
error: null,
loading: false,
success: false,
};
validate = (field, value) => {
if (!allFields.includes(field)) {
return null;
}
if (requiredFields.includes(field) && !value) {
return t('error.required_field');
}
if (field in validate) {
return validate[field](value) ? null : errorMsg[field];
}
return null;
};
signUp = ({ username, email, password }, redirectUri) => {
if (!redirectUri) {
redirectUri = this.context.pym.parentUrl || location.href;
}
const { rest } = this.context;
const params = {
method: 'POST',
body: {
username,
email,
password,
},
headers: { 'X-Pym-Url': redirectUri },
};
rest('/users', params)
.then(() => {
this.setState({ success: true, loading: false, error: null });
})
.catch(error => {
if (!error.status || error.status !== 401) {
console.error(error);
}
const changeSet = { success: false, loading: false, error };
this.setState(changeSet);
});
};
getErrorMessage() {
if (!this.state.error) {
return null;
}
return translateError(this.state.error);
}
render() {
return (
<WrappedComponent
{...this.props}
signUp={this.signUp}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
requireEmailConfirmation={
!!get(this.props, 'root.settings.requireEmailConfirmation')
}
success={this.state.success}
validate={this.validate}
/>
);
}
}
return WithSignUp;
});
export default compose(withSettingsQuery, withSignUp);
+61
View File
@@ -0,0 +1,61 @@
import * as actions from '../constants/auth';
import merge from 'lodash/merge';
const initialState = {
checkedInitialLogin: false,
initialLoginError: null,
user: null,
};
const purge = user => {
const {settings, ...userData} = user; // eslint-disable-line
return userData;
};
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
initialLoginError: action.error,
checkedInitialLogin: true,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
checkedInitialLogin: true,
user: action.user ? purge(action.user) : null,
};
case actions.HANDLE_SUCCESSFUL_LOGIN:
return {
...state,
user: action.user ? purge(action.user) : null,
};
case actions.LOGOUT:
return {
...state,
user: null,
};
case actions.UPDATE_STATUS: {
return {
...state,
user: {
...state.user,
status: merge({}, state.user.status, action.status),
},
};
}
case actions.UPDATE_USERNAME:
return {
...state,
user: {
...state.user,
username: action.username,
lowercaseUsername: action.username.toLowerCase(),
},
};
default:
return state;
}
}
@@ -1,15 +1,13 @@
import * as actions from '../actions/config';
import { MERGE_CONFIG } from '../constants/config';
const initialState = {
data: {},
};
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
case actions.CONFIG_UPDATED:
case MERGE_CONFIG:
return {
...state,
data: action.data,
...action.config,
};
default:
return state;
+8
View File
@@ -0,0 +1,8 @@
import auth from './auth';
import config from './config';
export default {
auth,
login: auth,
config,
};
+55 -4
View File
@@ -22,6 +22,10 @@ import {
import { createHistory } from 'coral-framework/services/history';
import { createIntrospection } from 'coral-framework/services/introspection';
import introspectionData from 'coral-framework/graphql/introspection.json';
import coreReducers from '../reducers';
import { checkLogin as checkLoginAction } from '../actions/auth';
import { mergeConfig } from '../actions/config';
import { setAuthToken, logout } from '../actions/auth';
/**
* getAuthToken returns the active auth token or null
@@ -32,7 +36,7 @@ import introspectionData from 'coral-framework/graphql/introspection.json';
const getAuthToken = (store, storage) => {
let state = store.getState();
if (state.config.auth_token) {
if (state.config && state.config.auth_token) {
// if an auth_token exists in config, use it.
return state.config.auth_token;
} else if (!bowser.safari && !bowser.ios && storage) {
@@ -51,6 +55,19 @@ function areWeInIframe() {
}
}
function initExternalConfig({ store, pym, inIframe }) {
if (!inIframe) {
return;
}
return new Promise(resolve => {
pym.sendMessage('getConfig');
pym.onMessage('config', config => {
store.dispatch(mergeConfig(JSON.parse(config)));
resolve();
});
});
}
/**
* createContext setups and returns Talk dependencies that should be
* passed to `TalkProvider`.
@@ -70,6 +87,8 @@ export async function createContext({
notification,
preInit,
init = noop,
checkLogin = true,
addExternalConfig = true,
} = {}) {
const inIframe = areWeInIframe();
const eventEmitter = new EventEmitter({ wildcard: true });
@@ -98,7 +117,8 @@ export async function createContext({
token,
});
let { LIVE_URI: liveUri } = getStaticConfiguration();
const staticConfig = getStaticConfiguration();
let { LIVE_URI: liveUri } = staticConfig;
if (liveUri == null) {
// The protocol must match the origin protocol, secure/insecure.
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
@@ -161,6 +181,7 @@ export async function createContext({
// Create our redux store.
const finalReducers = {
...coreReducers,
...reducers,
...plugins.getReducers(),
};
@@ -180,9 +201,39 @@ export async function createContext({
[client.middleware(), apolloErrorReporter, createReduxEmitter(eventEmitter)]
);
// Run pre initialization.
if (inIframe) {
pym.onMessage('login', token => {
if (token) {
store.dispatch(setAuthToken(token));
}
});
pym.onMessage('logout', () => {
store.dispatch(logout());
});
}
const preInitList = [];
store.dispatch(
mergeConfig({
static: staticConfig,
})
);
if (preInit) {
await preInit(context);
preInitList.push(preInit(context));
}
if (addExternalConfig) {
preInitList.push(initExternalConfig(context));
}
// Run pre initialization.
await Promise.all(preInitList);
if (checkLogin) {
store.dispatch(checkLoginAction());
}
// Run initialization.
+3 -2
View File
@@ -6,6 +6,7 @@ import flattenDeep from 'lodash/flattenDeep';
import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import mapValues from 'lodash/mapValues';
import get from 'lodash/get';
import { getDisplayName } from 'coral-framework/helpers/hoc';
import camelize from '../helpers/camelize';
@@ -83,7 +84,7 @@ class PluginsService {
* query datas are only passed to the component if it is defined in `component.fragments`.
*/
getSlotComponentProps(component, reduxState, props, queryData) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
return {
...props,
config: pluginConfig,
@@ -97,7 +98,7 @@ class PluginsService {
* Returns React Elements for given slot.
*/
getSlotElements(slot, reduxState, props = {}, queryData = {}) {
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
const isDisabled = component => {
if (
+9
View File
@@ -252,3 +252,12 @@ export function mapLeaves(o, mapper) {
return mapper(val);
});
}
export function translateError(error) {
if (error.translation_key) {
return t(`error.${error.translation_key}`);
} else if (error.networkError) {
return t('error.network_error');
}
return error.toString();
}
+1 -1
View File
@@ -21,7 +21,7 @@ export const getReliability = reliabilityValue => {
*/
export const isSuspended = user => {
const suspensionUntil = get(user, 'status.suspension.until');
const suspensionUntil = get(user, 'state.status.suspension.until');
return user && suspensionUntil && new Date(suspensionUntil) > new Date();
};
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import Slot from 'coral-framework/components/Slot';
class Main extends React.Component {
render() {
return <Slot fill="login" />;
}
}
export default Main;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { render } from 'react-dom';
import { createContext } from 'coral-framework/services/bootstrap';
import Main from './containers/Main';
import TalkProvider from 'coral-framework/components/TalkProvider';
import pluginsConfig from 'pluginsConfig';
async function main() {
const context = await createContext({
pluginsConfig,
});
render(
<TalkProvider {...context}>
<Main />
</TalkProvider>,
document.querySelector('#talk-login-container')
);
}
main();
@@ -0,0 +1,3 @@
.bare {
composes: buttonReset from "coral-framework/styles/reset.css";
}
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './BareButton.css';
import cn from 'classnames';
/**
* BareButton is a button whose styling is stripped off to a minimum.
* Can pass anchor=true to use `a` instead of `button`
*/
const BareButton = ({ anchor, className, ...props }) => {
let Element = 'button';
if (anchor) {
Element = 'a';
}
return <Element {...props} className={cn(styles.bare, className)} />;
};
BareButton.propTypes = {
className: PropTypes.string,
anchor: PropTypes.bool,
};
export default BareButton;
+1
View File
@@ -10,6 +10,7 @@
line-height: 22px;
min-width: 80px;
text-align: center;
vertical-align: text-top;
}
.icon {
+1
View File
@@ -28,3 +28,4 @@ export { default as Label } from './components/Label';
export { default as FlagLabel } from './components/FlagLabel';
export { default as Dropdown } from './components/Dropdown';
export { default as Option } from './components/Option';
export { default as BareButton } from './components/BareButton';
+4
View File
@@ -44,6 +44,10 @@ const CONFIG = {
// fetching again.
SETTINGS_CACHE_TIME: ms(process.env.TALK_SETTINGS_CACHE_TIME || '1hr'),
// ALLOW_NO_LIMIT_QUERIES enables some queries to specify a limit of -1 to
// request all of the records. Otherwise, minimum limits of 0 are enforced.
ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE',
//------------------------------------------------------------------------------
// JWT based configuration
//------------------------------------------------------------------------------
@@ -84,23 +84,3 @@ TALK_JWT_SECRET=jX9y8G2ApcVLwyL{$6s3
Be default, we sign our tokens with HMAC using a SHA-256 hash algorithm. If you
want to change the signing algorithm, or use multiple signing/verifying keys,
refer to our [Advanced Configuration]({{ "/advanced-configuration/" | relative_url }}) documentation.
## TALK_FACEBOOK_APP_ID
The Facebook App ID for your Facebook Login enabled app. You can learn more
about getting a Facebook App ID at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_FACEBOOK_APP_SECRET
The Facebook App Secret for your Facebook Login enabled app. You can learn more
about getting a Facebook App Secret at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
+53 -1
View File
@@ -57,6 +57,26 @@ When `TRUE`, it will not mount the static asset serving routes on the router.
This is used primarily in conjunction with [TALK_STATIC_URI](#talk_static_uri){: .param}
when the static assets are being hosted on an external domain. (Default `FALSE`)
## TALK_FACEBOOK_APP_ID
The Facebook App ID for your Facebook Login enabled app. You can learn more
about getting a Facebook App ID at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_FACEBOOK_APP_SECRET
The Facebook App Secret for your Facebook Login enabled app. You can learn more
about getting a Facebook App Secret at the
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
or by visiting the
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_HELMET_CONFIGURATION
A JSON string representing the configuration passed to the
@@ -303,6 +323,8 @@ the websocket to keep the socket alive, parsed by
## TALK_RECAPTCHA_PUBLIC
Setting a reCAPTCHA Public and Secret key will enable and require reCAPTCHA upon multiple failed login attempts.
Client secret used for enabling reCAPTCHA powered logins. If
[TALK_RECAPTCHA_SECRET](#talk_recaptcha_secret){: .param} and
[TALK_RECAPTCHA_PUBLIC](#talk_recaptcha_public){: .param} are not provided it will instead
@@ -486,4 +508,34 @@ Used to set the key for use with
[Apollo Engine](https://www.apollographql.com/engine/){:target="_blank"} for
tracing of GraphQL requests.
**Note: Apollo Engine is a premium service, charges may apply.**
**Note: Apollo Engine is a premium service, charges may apply.**
## ALLOW_NO_LIMIT_QUERIES
Setting this to `TRUE` will allow queries to execute without a limit (returns
all documents). This introduces a significant performance regression, and should
be used with caution. (Default `FALSE`)
## TALK_ADDTL_COMMENTS_ON_LOAD_MORE
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10`.
Specifies the number of additional comments to load when a user clicks `Load More`. (Default `10`)
## TALK_ASSET_COMMENTS_LOAD_DEPTH
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_ASSET_COMMENTS_LOAD_DEPTH=10`.
Specifies the initial number of comments to load for an asset. (Default `10`)
## TALK_REPLY_COMMENTS_LOAD_DEPTH
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_REPLY_COMMENTS_LOAD_DEPTH=3`.
Specifies the initial replies to load for a comment. (Default `3`)
+158 -28
View File
@@ -1,54 +1,184 @@
const DataLoader = require('dataloader');
const util = require('./util');
const ActionsService = require('../../services/actions');
const ActionModel = require('../../models/action');
const { first, get, merge, remove, groupBy, reduce, isNil } = require('lodash');
/**
* Gets actions based on their item id's.
*/
const genActionsByItemID = (_, item_ids) => {
return ActionsService.findByItemIdArray(item_ids).then(
const genActionsByItemID = (
{ connectors: { services: { Actions } } },
item_ids
) => {
return Actions.findByItemIdArray(item_ids).then(
util.arrayJoinBy(item_ids, 'item_id')
);
};
/**
* Looks up actions based on the requested id's all bounded by the user.
* @param {Object} context the context of the request
* @param {Array} ids array of id's to get
* @return {Promise} resolves to the promises of the requested actions
* Looks up the actions for each of the items.
*
* @param {Object} ctx the graph context of the request
* @param {Array<String>} itemIDs the items that we need to get the actions for
*/
const genActionSummariessByItemID = ({ user = {} }, item_ids) => {
return ActionsService.getActionSummaries(item_ids, user.id).then(
util.arrayJoinBy(item_ids, 'item_id')
const genActionsAuthoredWithID = (
{ user = {}, connectors: { services: { Actions } } },
itemIDs
) =>
Actions.getUserActions(user.id, itemIDs).then(
util.arrayJoinBy(itemIDs, 'item_id')
);
};
/**
* Search for actions based on their action_type and item_type and ensures that
* the actions returned have unique item id's.
* @param {String} action_type the action to search by
* @param {String} item_type the item id to search by
* @return {Promise} resolves to distinct items actions
* iterateActionCounts will create an iterable object that can be used to
* compute action summaries.
*
* @param {Object} action_counts the action count object
*/
const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
return ActionModel.distinct('item_id', { action_type, item_type });
const iterateActionCounts = action_counts =>
!isNil(action_counts)
? Object.keys(action_counts).map(action_type => ({
count: action_counts[action_type],
action_type: action_type.toUpperCase(),
}))
: [];
/**
* getUserActions will get the actions made by the user for this specific
* item.
*
* @param {Object} ctx the graph context of the request
* @param {Object} item the item that we're getting the actions for
*/
async function getUserActions(ctx, { action_counts, id }) {
const { loaders: { Actions } } = ctx;
// Get the total count for all action types.
const totalActionCount = reduce(
action_counts,
(total, count) => total + count,
0
);
// Check to see if there are any user actions to get.
const hasUserActions = ctx.user && totalActionCount > 0;
if (!hasUserActions) {
return {};
}
// Possibly get the list of user actions completed by the user. This will be
// used later to join together with the action summaries to provide context.
const userActions = await Actions.getAuthoredByID.load(id);
if (userActions.length === 0) {
return {};
}
// Group the user actions in the same way that the action counts are
// grouped. This will let us extract it easy.
return reduce(
groupBy(userActions, ({ action_type, group_id }) =>
(group_id ? `${action_type}_${group_id}` : action_type).toUpperCase()
),
(allUserActions, userActions, actionType) =>
merge(allUserActions, { [actionType]: first(userActions) }),
{}
);
}
// This will match any action count that is specific for a group id.
const nonGroupIDTest = /^([A-Z]+)_([A-Z_]+)$/;
/**
* resolveActionSummariesForItem will resolve the action summaries for an item.
*
* @param {Object} ctx the graph context of the request
* @param {Object} item the item that we are resolving an action summary for
*/
async function resolveActionSummariesForItem(ctx, { id, action_counts }) {
// Cache all those entries for which we got the group id of, because we
// don't want to include them twice.
const groupIDCache = {};
// Get the user actions for this specific item.
const groupedUserActions = await getUserActions(ctx, { id, action_counts });
// Generate the action summaries for the item.
return iterateActionCounts(action_counts).reduce(
(actionTypeList, { count, action_type }) => {
// Get the current user's actions (if they have any).
const current_user = get(groupedUserActions, action_type, null);
// Check to see if this is a action without a corresponding group id.
if (nonGroupIDTest.test(action_type)) {
// This action type does have a group id associated with it.
const results = nonGroupIDTest.exec(action_type);
const groupActionType = results[1];
const groupID = results[2];
// Purge out the summary if it already exists, and mark that this
// group id has been found so we don't include it in the future.
remove(
actionTypeList,
({ action_type }) => action_type === groupActionType
);
groupIDCache[groupActionType] = true;
// Push the new entry in.
actionTypeList.push({
action_type: groupActionType,
group_id: groupID,
count,
current_user,
});
} else {
// This does not have a group id. Check to see if this group id
// already has an specific (group id) entry.
if (groupIDCache[action_type]) {
// It does. Don't add anything.
return actionTypeList;
}
// It does not, add the entry.
actionTypeList.push({
action_type,
group_id: null,
count,
current_user,
});
}
return actionTypeList;
},
[]
);
}
/**
* Looks up the action summaries for a set of items.
*
* @param {Object} ctx the graph context of the request
* @param {Array<Object>} items the items that should have their items looked up for
*/
const genActionSummariesByItem = async (ctx, items) => {
// This is designed to match the action_counts value that is embedded on
// documents which cache action counts. For users that are not logged in, we
// don't need to hit the actions collection at all!
// We will literate over all the items that we're comparing.
return items.map(item => resolveActionSummariesForItem(ctx, item));
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @param {Object} ctx the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = context => ({
module.exports = ctx => ({
Actions: {
getByID: new DataLoader(ids => genActionsByItemID(context, ids)),
getSummariesByItemID: new DataLoader(ids =>
genActionSummariessByItemID(context, ids)
getByID: new DataLoader(ids => genActionsByItemID(ctx, ids)),
getSummariesByItem: new DataLoader(
items => genActionSummariesByItem(ctx, items),
{ cacheKeyFn: ({ id }) => id }
),
getByTypes: ({ action_type, item_type }) =>
getItemIdsByActionTypeAndItemType(context, action_type, item_type),
getAuthoredByID: new DataLoader(ids => genActionsAuthoredWithID(ctx, ids)),
},
});
+44 -43
View File
@@ -4,7 +4,10 @@ const {
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
SEARCH_OTHERS_COMMENTS,
} = require('../../perms/constants');
const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config');
const {
CACHE_EXPIRY_COMMENT_COUNT,
ALLOW_NO_LIMIT_QUERIES,
} = require('../../config');
const ms = require('ms');
const sc = require('snake-case');
@@ -148,12 +151,11 @@ const getCommentCountByQuery = (ctx, options) => {
* @param {Object} params the params from the client describing the query
*/
const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[0].created_at : null;
case 'REPLIES':
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
if (sortBy === 'CREATED_AT') {
return nodes.length ? nodes[0].created_at : null;
} else if (sortBy === 'REPLIES') {
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
}
const SORT_KEY = sortBy.toLowerCase();
@@ -181,11 +183,10 @@ const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
* @param {Object} params the params from the client describing the query
*/
const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[nodes.length - 1].created_at : null;
case 'REPLIES':
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
if (sortBy === 'CREATED_AT') {
return nodes.length ? nodes[nodes.length - 1].created_at : null;
} else if (sortBy === 'REPLIES') {
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
}
const SORT_KEY = sortBy.toLowerCase();
@@ -212,36 +213,33 @@ const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
* @param {Object} params the params from the client describing the query
*/
const applySort = (ctx, query, { cursor, sortOrder, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT': {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
if (sortBy === 'CREATED_AT') {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
}
case 'REPLIES': {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
} else if (sortBy === 'REPLIES') {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
}
const SORT_KEY = sortBy.toLowerCase();
@@ -280,7 +278,7 @@ const executeWithSort = async (
query = applySort(ctx, query, { cursor, sortOrder, sortBy });
// Apply the limit (if it exists, as it's applied universally).
if (limit) {
if (limit >= 0) {
query = query.limit(limit + 1);
}
@@ -290,7 +288,7 @@ const executeWithSort = async (
// The hasNextPage is always handled the same (ask for one more than we need,
// if there is one more, than there is more).
let hasNextPage = false;
if (limit && nodes.length > limit) {
if (limit >= 0 && nodes.length > limit) {
// There was one more than we expected! Set hasNextPage = true and remove
// the last item from the array that we requested.
hasNextPage = true;
@@ -302,11 +300,9 @@ const executeWithSort = async (
return {
startCursor: getStartCursor(ctx, nodes, {
cursor,
sortOrder,
sortBy,
limit,
}),
endCursor: getEndCursor(ctx, nodes, { cursor, sortOrder, sortBy, limit }),
endCursor: getEndCursor(ctx, nodes, { cursor, sortBy }),
hasNextPage,
nodes,
};
@@ -338,6 +334,11 @@ const getCommentsByQuery = async (
) => {
let comments = CommentModel.find();
// Enforce that the limit must be gte 0 if this option is not true.
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
throw new Error('cannot query for limit < 0');
}
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
// special privileges.
if (
+4 -4
View File
@@ -40,12 +40,12 @@ const Comment = {
return Actions.getByID.load(id);
},
action_summaries({ id, action_summaries }, _, { loaders: { Actions } }) {
if (action_summaries) {
return action_summaries;
action_summaries(comment, _, { loaders: { Actions } }) {
if (comment.action_summaries) {
return comment.action_summaries;
}
return Actions.getSummariesByItemID.load(id);
return Actions.getSummariesByItem.load(comment);
},
asset({ asset_id }, _, { loaders: { Assets } }) {
return Assets.getByID.load(asset_id);
+2 -2
View File
@@ -10,8 +10,8 @@ const {
} = require('../../perms/constants');
const User = {
action_summaries({ id }, _, { loaders: { Actions } }) {
return Actions.getSummariesByItemID.load(id);
action_summaries(user, _, { loaders: { Actions } }) {
return Actions.getSummariesByItem.load(user);
},
actions({ id }, _, { user, loaders: { Actions } }) {
// Only return the actions if the user is not an admin.
+2
View File
@@ -23,6 +23,7 @@ en:
click_to_confirm: "Click below to confirm your email address"
confirm: "Confirm"
password_reset:
mail_sent: 'If you have a registered account, a password reset link was sent to that email'
set_new_password: "Change Your Password"
new_password: "New Password"
new_password_help: "Password must be at least 8 characters"
@@ -236,6 +237,7 @@ en:
password: "Password must be at least 8 characters"
username: "Usernames can contain letters numbers and _ only"
unexpected: "Unexpected error occurred. Sorry!"
required_field: "This field is required"
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "Report comment"
flag_reason: "Reason for reporting (Optional)"
+2 -1
View File
@@ -27,7 +27,7 @@ es:
new_password: "New Password"
new_password_help: "Password must be at least 8 characters"
confirm_new_password: "Confirm New Password"
change_password: "Change Password"
change_password: "Change Password"
characters_remaining: "carácteres restantes"
comment:
anon: Anónimo
@@ -235,6 +235,7 @@ es:
organization_name: "El nombre de la organización debe contener letras y/o números."
password: "La contraseña debe tener por lo menos 8 caracteres"
username: "Los nombres pueden contener letras números y _"
required_field: "Este campo es requerido"
unexpected: "Lo siento. Ha habido un error no previsto."
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "Reportar este comentario"
+2 -1
View File
@@ -2,7 +2,7 @@ fr:
your_account_has_been_suspended: Your account has been temporarily suspended.
your_account_has_been_banned: Your account has been banned.
your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.
embed_comments_tab: Comments
embed_comments_tab: Comments
bandialog:
are_you_sure: "Êtes-vous sûr de vouloir bannir {0}?"
ban_user: "Bannir l'utilisateur ?"
@@ -235,6 +235,7 @@ fr:
organization_name: "Le nom de l'organisation ne peut contenir que des lettres ou des chiffres."
password: "Le mot de passe doit être d'au moins 8 caractères"
username: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\""
required_field: "Ce champ est obligatoire"
unexpected: "Unexpected error occurred. Sorry!"
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "Signaler un commentaire"
+1
View File
@@ -236,6 +236,7 @@ zh_CN:
password: "密码长度须至少为 8 字符"
username: "用户名只能包含字母、数字跟下划线"
unexpected: "发生了异常错误。对不起!"
required_field: "该字段必填"
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "举报评论"
flag_reason: "举报理由(可选)"
+1
View File
@@ -236,6 +236,7 @@ zh_TW:
password: "密碼必須至少8個字符"
username: "用戶名只能包含字母、數字和下劃線。"
unexpected: "發生了意外錯誤。抱歉!"
required_field: "該字段必填"
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
flag_comment: "舉報評論"
flag_reason: "舉報原因(可選)"
-11
View File
@@ -1,11 +0,0 @@
const pubsub = require('../services/pubsub');
// To handle dependancy injection safer, we inject the pubsub handle onto the
// request object.
module.exports = (req, res, next) => {
// Attach the pubsub handle to the requests.
req.pubsub = pubsub.getClient();
// Forward on the request.
next();
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "4.2.0",
"version": "4.2.2",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
@@ -117,9 +117,9 @@
"inquirer": "^3.2.2",
"inquirer-autocomplete-prompt": "^0.12.1",
"ioredis": "3.1.4",
"joi": "^10.6.0",
"joi": "^13.0.0",
"json-loader": "^0.5.7",
"jsonwebtoken": "^7.4.3",
"jsonwebtoken": "^8.0.0",
"jwt-decode": "^2.2.0",
"keymaster": "^1.6.2",
"kue": "0.11.6",
+5
View File
@@ -0,0 +1,5 @@
export {
setAuthToken,
handleSuccessfulLogin,
logout,
} from 'coral-framework/actions/auth';
+1
View File
@@ -1 +1,2 @@
export { setSort } from 'coral-embed-stream/src/actions/stream';
export { showSignInDialog } from 'coral-embed-stream/src/actions/login';
@@ -26,3 +26,4 @@ export {
export {
default as StreamConfiguration,
} from 'coral-framework/components/StreamConfiguration';
export { default as Recaptcha } from 'coral-framework/components/Recaptcha';
+11 -4
View File
@@ -1,10 +1,17 @@
export { default as withReaction } from './withReaction';
export { default as withTags } from './withTags';
export { default as withSortOption } from './withSortOption';
export { default as withFragments } from 'coral-framework/hocs/withFragments';
export { default as excludeIf } from 'coral-framework/hocs/excludeIf';
export { default as connect } from 'coral-framework/hocs/connect';
export { default as withEmit } from 'coral-framework/hocs/withEmit';
export {
connect,
withEmit,
excludeIf,
withFragments,
withForgotPassword,
withSignIn,
withSignUp,
withResendEmailConfirmation,
withSetUsername,
} from 'coral-framework/hocs';
export {
withIgnoreUser,
withBanUser,
+1 -2
View File
@@ -15,8 +15,7 @@ import * as PropTypes from 'prop-types';
import { getDefinitionName } from '../utils';
import { t, can } from 'plugin-api/beta/client/services';
// TODO: Auth logic needs refactoring.
import { showSignInDialog } from 'coral-embed-stream/src/actions/auth';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
/*
* Disable false-positive warning below, as it doesn't work well with how we currently
+8
View File
@@ -0,0 +1,8 @@
import get from 'lodash/get';
export const usernameStatusSelector = state =>
get(state, 'auth.user.status.username.status');
export const usernameSelector = state => get(state, 'auth.user.username');
export const isLoggedInSelector = state => !!get(state, 'auth.user');
-1
View File
@@ -1,7 +1,6 @@
{
"server": [
"talk-plugin-auth",
"talk-plugin-facebook-auth",
"talk-plugin-featured-comments",
"talk-plugin-offtopic",
"talk-plugin-respect"
@@ -5,7 +5,7 @@ import { t } from 'plugin-api/beta/client/services';
const SpamLabel = () => (
<CommentDetail
icon={'add_box'}
icon={'bug_report'}
header={t('talk-plugin-akismet.spam_comment')}
info={t('talk-plugin-akismet.detected')}
/>
@@ -3,7 +3,7 @@ import { FlagLabel } from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
const SpamLabel = () => (
<FlagLabel iconName="add_box">{t('talk-plugin-akismet.spam')}</FlagLabel>
<FlagLabel iconName="bug_report">{t('talk-plugin-akismet.spam')}</FlagLabel>
);
export default SpamLabel;
@@ -5,7 +5,7 @@ import { isSpam } from '../utils';
const enhance = compose(
excludeIf(
({ comment: { spam, actions } }) => spam === null || isSpam(actions)
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
)
);
@@ -1,12 +0,0 @@
import { compose } from 'react-apollo';
import { excludeIf } from 'plugin-api/beta/client/hocs';
import SpamDetail from './SpamDetail';
import { isSpam } from '../utils';
const enhance = compose(
excludeIf(
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
)
);
export default enhance(SpamDetail);
@@ -2,7 +2,6 @@ import translations from './translations.yml';
import CheckSpamHook from './containers/CheckSpamHook';
import SpamLabel from './containers/SpamLabel';
import SpamCommentDetail from './containers/SpamCommentDetail';
import SpamCommentFlagDetail from './containers/SpamCommentFlagDetail';
export default {
translations,
@@ -10,6 +9,5 @@ export default {
commentInputDetailArea: [CheckSpamHook],
adminCommentLabels: [SpamLabel],
adminCommentMoreDetails: [SpamCommentDetail],
adminCommentMoreFlagDetails: [SpamCommentFlagDetail],
},
};
@@ -68,7 +68,7 @@ nl_NL:
spam_comment: "Spam"
detected: "Gedetecteerd door Akismet"
still_spam: |
Dank je wel. Ons moderatieteam zal je reactie beoordelen.
Dank je wel. Ons moderatieteam zal je reactie beoordelen.
flags:
reasons:
comment:
@@ -1,183 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import errorMsj from 'coral-framework/helpers/error';
import validate from 'coral-framework/helpers/validate';
import CreateUsernameDialog from './CreateUsernameDialog';
import { withSetUsername } from 'coral-framework/graphql/mutations';
import { forEachError } from 'plugin-api/beta/client/utils';
import t from 'coral-framework/services/i18n';
import {
showCreateUsernameDialog,
hideCreateUsernameDialog,
invalidForm,
validForm,
updateUsername,
} from 'coral-embed-stream/src/actions/auth';
class ChangeUsernameContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
formData: {
username: (props.auth.user && props.auth.user.username) || '',
},
errors: {},
showErrors: false,
};
}
componentWillReceiveProps(next) {
if (
!this.props.auth.showCreateUsernameDialog &&
next.auth.showCreateUsernameDialog
) {
this.setState({
formData: {
username:
(this.props.auth.user && this.props.auth.user.username) || '',
},
});
}
}
handleChange = e => {
const { name, value } = e.target;
this.setState(
state => ({
...state,
formData: {
...state.formData,
[name]: value,
},
}),
() => {
this.validation(name, value);
}
);
};
addError = (name, error) => {
return this.setState(state => ({
errors: {
...state.errors,
[name]: error,
},
}));
};
validation = (name, value) => {
const { addError } = this;
if (!value.length) {
addError(name, t('createdisplay.required_field'));
} else if (!validate[name](value)) {
addError(name, errorMsj[name]);
} else {
const {[name]: prop, ...errors} = this.state.errors; // eslint-disable-line
// Removes Error
this.setState(state => ({ ...state, errors }));
}
};
isCompleted = () => {
const { formData } = this.state;
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
};
displayErrors = (show = true) => {
this.setState({ showErrors: show });
};
async setUsernameAndClose(username, props = this.props) {
const {
validForm,
invalidForm,
setUsername,
hideCreateUsernameDialog,
updateUsername,
} = props;
try {
// Perform mutation
await setUsername(this.props.auth.user.id, username);
// Also change in redux store...
updateUsername(username);
hideCreateUsernameDialog();
validForm();
} catch (error) {
const msgs = [];
forEachError(error, ({ msg }) => msgs.push(msg));
invalidForm(t(msgs.join(', ')));
}
}
handleSubmitUsername = e => {
e.preventDefault();
const { errors, formData: { username } } = this.state;
const { invalidForm } = this.props;
this.displayErrors();
if (this.isCompleted() && !Object.keys(errors).length) {
this.setUsernameAndClose(username);
} else {
invalidForm(t('createdisplay.check_the_form'));
}
};
handleClose = () => {
this.setUsernameAndClose(this.props.auth.user.username);
};
render() {
const { loggedIn, auth } = this.props;
return (
<div>
<CreateUsernameDialog
open={auth.showCreateUsernameDialog}
handleClose={this.handleClose}
loggedIn={loggedIn}
handleSubmitUsername={this.handleSubmitUsername}
{...this}
{...this.state}
{...this.props}
/>
</div>
);
}
}
ChangeUsernameContainer.propTypes = {
auth: PropTypes.object,
hideCreateUsernameDialog: PropTypes.func,
validForm: PropTypes.func,
invalidForm: PropTypes.func,
loggedIn: PropTypes.bool,
changeUsername: PropTypes.func,
};
const mapStateToProps = ({ auth }) => ({
auth: auth,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showCreateUsernameDialog,
hideCreateUsernameDialog,
invalidForm,
validForm,
updateUsername,
},
dispatch
);
export default compose(
withSetUsername,
connect(mapStateToProps, mapDispatchToProps)
)(ChangeUsernameContainer);

Some files were not shown because too many files have changed in this diff Show More