diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js deleted file mode 100644 index 193c6ac44..000000000 --- a/client/coral-admin/src/actions/auth.js +++ /dev/null @@ -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' }); -}; diff --git a/client/coral-admin/src/actions/config.js b/client/coral-admin/src/actions/config.js deleted file mode 100644 index 5e2995e85..000000000 --- a/client/coral-admin/src/actions/config.js +++ /dev/null @@ -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 }); -}; diff --git a/client/coral-admin/src/components/Drawer.js b/client/coral-admin/src/components/Drawer.js index 0d59d3bbe..25f21a455 100644 --- a/client/coral-admin/src/components/Drawer.js +++ b/client/coral-admin/src/components/Drawer.js @@ -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 }) => ( - {auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? ( + {currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
- {can(auth.user, 'MODERATE_COMMENTS') && ( + {can(currentUser, 'MODERATE_COMMENTS') && ( ( > {t('configure.community')} - {can(auth.user, 'UPDATE_CONFIG') && ( + {can(currentUser, 'UPDATE_CONFIG') && ( ( 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; diff --git a/client/coral-admin/src/components/ForgotPassword.css b/client/coral-admin/src/components/ForgotPassword.css new file mode 100644 index 000000000..beae79eea --- /dev/null +++ b/client/coral-admin/src/components/ForgotPassword.css @@ -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; +} diff --git a/client/coral-admin/src/components/ForgotPassword.js b/client/coral-admin/src/components/ForgotPassword.js new file mode 100644 index 000000000..ad90c5d6a --- /dev/null +++ b/client/coral-admin/src/components/ForgotPassword.js @@ -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 ( +
+ {t('password_reset.mail_sent')}{' '} + + Sign in + + +
+ ); + } + + renderForm() { + const { email, errorMessage } = this.props; + return ( +
+ {errorMessage && {errorMessage}} + + +

+ Go back to{' '} + + Sign In + + . +

+ + ); + } + + 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.isRequired, + onSignInLink: PropTypes.func.isRequired, +}; + +export default ForgotPassword; diff --git a/client/coral-admin/src/components/Header.js b/client/coral-admin/src/components/Header.js index 6b2274d27..fc150b840 100644 --- a/client/coral-admin/src/components/Header.js +++ b/client/coral-admin/src/components/Header.js @@ -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 = ({
- {auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? ( + {currentUser && can(currentUser, 'ACCESS_ADMIN') ? ( - {can(auth.user, 'MODERATE_COMMENTS') && ( + {can(currentUser, 'MODERATE_COMMENTS') && ( - {can(auth.user, 'UPDATE_CONFIG') && ( + {can(currentUser, 'UPDATE_CONFIG') && ( - - {t('configure.sign_out')} - + {currentUser && ( + + {t('configure.sign_out')} + + )}
@@ -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, diff --git a/client/coral-admin/src/components/Layout.js b/client/coral-admin/src/components/Layout.js index 900858dee..5e3b9ce03 100644 --- a/client/coral-admin/src/components/Layout.js +++ b/client/coral-admin/src/components/Layout.js @@ -10,22 +10,26 @@ const Layout = ({ handleLogout = () => {}, toggleShortcutModal = () => {}, restricted = false, - auth, + currentUser, }) => (
+ -
{children}
); 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 diff --git a/client/coral-admin/src/components/Login.css b/client/coral-admin/src/components/Login.css new file mode 100644 index 000000000..49f03a11f --- /dev/null +++ b/client/coral-admin/src/components/Login.css @@ -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; +} + diff --git a/client/coral-admin/src/components/Login.js b/client/coral-admin/src/components/Login.js new file mode 100644 index 000000000..cc5ec9436 --- /dev/null +++ b/client/coral-admin/src/components/Login.js @@ -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 ? ( + + ) : ( + + ); + } + + render() { + return ( + +
+

Team sign in

+

Sign in to interact with your community.

+ {this.renderForm()} +
+
+ ); + } +} + +LoginContainer.propTypes = { + forgotPassword: PropTypes.bool.isRequired, + onForgotPasswordLink: PropTypes.func.isRequired, + onSignInLink: PropTypes.func.isRequired, +}; + +export default LoginContainer; diff --git a/client/coral-admin/src/components/NotFound.css b/client/coral-admin/src/components/NotFound.css index 9977f858a..6743a144d 100644 --- a/client/coral-admin/src/components/NotFound.css +++ b/client/coral-admin/src/components/NotFound.css @@ -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; -} diff --git a/client/coral-admin/src/components/SignIn.css b/client/coral-admin/src/components/SignIn.css new file mode 100644 index 000000000..10ab54c09 --- /dev/null +++ b/client/coral-admin/src/components/SignIn.css @@ -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; +} diff --git a/client/coral-admin/src/components/SignIn.js b/client/coral-admin/src/components/SignIn.js new file mode 100644 index 000000000..1525145a7 --- /dev/null +++ b/client/coral-admin/src/components/SignIn.js @@ -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 ( +
+ {errorMessage && {errorMessage}} + + + {requireRecaptcha && ( +
+ +
+ )} + +

+ Forgot your password?{' '} + + Request a new one. + +

+ + ); + } +} + +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.isRequired, + requireRecaptcha: PropTypes.bool.isRequired, +}; + +export default SignIn; diff --git a/client/coral-admin/src/containers/ForgotPassword.js b/client/coral-admin/src/containers/ForgotPassword.js new file mode 100644 index 000000000..558747ab9 --- /dev/null +++ b/client/coral-admin/src/containers/ForgotPassword.js @@ -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 ( + + ); + } +} + +ForgotPasswordContainer.propTypes = { + success: PropTypes.bool.isRequired, + forgotPassword: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + onSignInLink: PropTypes.func.isRequired, +}; + +export default compose(withForgotPassword)(ForgotPasswordContainer); diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index 07e780e3e..aad9971c4 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -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 ; } - if (!loggedIn) { - return ( - - ); + if (!currentUser) { + return ; } - if (can(user, 'ACCESS_ADMIN') && loggedIn) { - return ( - - - - - {children} - - ); - } else if (loggedIn) { - return ( - -

- This page is for team use only. Please contact an administrator if - you want to join this team. -

-
- ); + if (currentUser) { + if (can(currentUser, 'ACCESS_ADMIN')) { + return ( + + + + + {children} + + ); + } else { + return ( + +

+ This page is for team use only. Please contact an administrator if + you want to join this team. +

+
+ ); + } } return ; } @@ -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, }, diff --git a/client/coral-admin/src/containers/Login.js b/client/coral-admin/src/containers/Login.js new file mode 100644 index 000000000..08558bc5c --- /dev/null +++ b/client/coral-admin/src/containers/Login.js @@ -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 ( + + ); + } +} + +LoginContainer.propTypes = {}; + +export default LoginContainer; diff --git a/client/coral-admin/src/containers/SignIn.js b/client/coral-admin/src/containers/SignIn.js new file mode 100644 index 000000000..a48b15ce7 --- /dev/null +++ b/client/coral-admin/src/containers/SignIn.js @@ -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 ( + + ); + } +} + +SignInContainer.propTypes = { + signIn: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + onForgotPasswordLink: PropTypes.func.isRequired, + requireRecaptcha: PropTypes.bool.isRequired, +}; + +export default compose(withSignIn)(SignInContainer); diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js deleted file mode 100644 index 14aa2c21c..000000000 --- a/client/coral-admin/src/reducers/auth.js +++ /dev/null @@ -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; - } -} diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 11801a379..be0a8a606 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -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, }; diff --git a/client/coral-admin/src/routes/Configure/components/Configure.js b/client/coral-admin/src/routes/Configure/components/Configure.js index f81bf7735..140222f68 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.js +++ b/client/coral-admin/src/routes/Configure/components/Configure.js @@ -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 (

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, diff --git a/client/coral-admin/src/routes/Configure/containers/Configure.js b/client/coral-admin/src/routes/Configure/containers/Configure.js index ce33fa1f4..fe87766b6 100644 --- a/client/coral-admin/src/routes/Configure/containers/Configure.js +++ b/client/coral-admin/src/routes/Configure/containers/Configure.js @@ -30,7 +30,7 @@ class ConfigureContainer extends Component { return ( ({ - 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, diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index c1de008fe..2d9b793ff 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -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, diff --git a/client/coral-admin/src/routes/Moderation/containers/Moderation.js b/client/coral-admin/src/routes/Moderation/containers/Moderation.js index c0e2e60a6..f35255732 100644 --- a/client/coral-admin/src/routes/Moderation/containers/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/containers/Moderation.js @@ -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 => ({ diff --git a/client/coral-embed-stream/src/AppRouter.js b/client/coral-embed-stream/src/AppRouter.js deleted file mode 100644 index ec7b6300a..000000000 --- a/client/coral-embed-stream/src/AppRouter.js +++ /dev/null @@ -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 = ( -

- - -
-); - -class AppRouter extends React.Component { - static contextTypes = { - history: PropTypes.object, - }; - - render() { - return ; - } -} - -export default AppRouter; diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js deleted file mode 100644 index d7e2b7826..000000000 --- a/client/coral-embed-stream/src/actions/auth.js +++ /dev/null @@ -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)); - }); -}; diff --git a/client/coral-embed-stream/src/actions/login.js b/client/coral-embed-stream/src/actions/login.js new file mode 100644 index 000000000..c602bf7b6 --- /dev/null +++ b/client/coral-embed-stream/src/actions/login.js @@ -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, +}); diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 0fd257ef2..b7eb6d580 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -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 = [ , ]; - if (can(user, 'UPDATE_ASSET_CONFIG')) { + if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) { tabs.push( ; } - return ; + return ( + + ); } } @@ -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); diff --git a/client/coral-embed-stream/src/containers/LoginContainer.js b/client/coral-embed-stream/src/containers/LoginContainer.js deleted file mode 100644 index 5ee0c54a1..000000000 --- a/client/coral-embed-stream/src/containers/LoginContainer.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import Slot from 'coral-framework/components/Slot'; - -export const LoginContainer = () => ; diff --git a/client/coral-embed-stream/src/index.js b/client/coral-embed-stream/src/index.js index 7100c0d28..a4c6bcd39 100644 --- a/client/coral-embed-stream/src/index.js +++ b/client/coral-embed-stream/src/index.js @@ -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( - + , document.querySelector('#talk-embed-stream-container') ); diff --git a/client/coral-embed-stream/src/reducers/auth.js b/client/coral-embed-stream/src/reducers/auth.js deleted file mode 100644 index faa576fff..000000000 --- a/client/coral-embed-stream/src/reducers/auth.js +++ /dev/null @@ -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; - } -} diff --git a/client/coral-embed-stream/src/reducers/index.js b/client/coral-embed-stream/src/reducers/index.js index ac581557b..039db7ec3 100644 --- a/client/coral-embed-stream/src/reducers/index.js +++ b/client/coral-embed-stream/src/reducers/index.js @@ -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, }; diff --git a/client/coral-embed-stream/src/reducers/login.js b/client/coral-embed-stream/src/reducers/login.js new file mode 100644 index 000000000..c24d36f96 --- /dev/null +++ b/client/coral-embed-stream/src/reducers/login.js @@ -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; + } +} diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 42af4453a..30b9fe22a 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -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); diff --git a/client/coral-embed-stream/src/tabs/profile/containers/ProfileContainer.js b/client/coral-embed-stream/src/tabs/profile/containers/ProfileContainer.js index a818ebec4..57aa2b0fe 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/ProfileContainer.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/ProfileContainer.js @@ -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
{this.props.data.error.message}
; } - if (!auth.loggedIn) { + if (!currentUser) { return ; } @@ -77,7 +66,7 @@ class ProfileContainer extends Component { return ; } - 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), diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index 23f438045..a431b1986 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -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 { )} {changedUsername && } - {!banned && rejectedUsername && } + {!banned && + rejectedUsername && } {banned && } {showCommentBox && ( @@ -312,10 +315,10 @@ class Stream extends React.Component { - {loggedIn && ( + {currentUser && ( )} @@ -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, diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js index 51b4d9d9e..06632dfcc 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { gql, compose } from 'react-apollo'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -14,8 +15,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 +41,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 +58,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 +90,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 +202,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 +223,28 @@ class StreamContainer extends React.Component { return ( ({ - auth: state.auth, + currentUser: state.auth.user, activeReplyBox: state.stream.activeReplyBox, commentId: state.stream.commentId, assetId: state.stream.assetId, @@ -417,7 +462,6 @@ const mapDispatchToProps = dispatch => showSignInDialog, notify, setActiveReplyBox, - editName, viewAllComments, setActiveStreamTab: setActiveTab, }, diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index a04ee6f05..04797211e 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -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 */ diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js new file mode 100644 index 000000000..0c4780548 --- /dev/null +++ b/client/coral-framework/actions/auth.js @@ -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, +}); diff --git a/client/coral-framework/actions/config.js b/client/coral-framework/actions/config.js new file mode 100644 index 000000000..dd1522333 --- /dev/null +++ b/client/coral-framework/actions/config.js @@ -0,0 +1,6 @@ +import { MERGE_CONFIG } from '../constants/config'; + +export const mergeConfig = config => ({ + type: MERGE_CONFIG, + config, +}); diff --git a/client/coral-framework/components/Recaptcha.js b/client/coral-framework/components/Recaptcha.js new file mode 100644 index 000000000..f632514ba --- /dev/null +++ b/client/coral-framework/components/Recaptcha.js @@ -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 ( + + ); + } +} + +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; diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index af524b21d..fe63b4f30 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -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, diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js new file mode 100644 index 000000000..25e2f6a78 --- /dev/null +++ b/client/coral-framework/constants/auth.js @@ -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'; diff --git a/client/coral-framework/constants/config.js b/client/coral-framework/constants/config.js new file mode 100644 index 000000000..bf846af1d --- /dev/null +++ b/client/coral-framework/constants/config.js @@ -0,0 +1,3 @@ +const prefix = `TALK_FRAMEWORK`; + +export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`; diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js index 174f25a63..eb481de99 100644 --- a/client/coral-framework/hocs/index.js +++ b/client/coral-framework/hocs/index.js @@ -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'; diff --git a/client/coral-framework/hocs/withForgotPassword.js b/client/coral-framework/hocs/withForgotPassword.js new file mode 100644 index 000000000..9ef4b59e1 --- /dev/null +++ b/client/coral-framework/hocs/withForgotPassword.js @@ -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 ( + + ); + } + } + + return WithForgotPassword; +}); diff --git a/client/coral-framework/hocs/withResendEmailConfirmation.js b/client/coral-framework/hocs/withResendEmailConfirmation.js new file mode 100644 index 000000000..64f5ed69f --- /dev/null +++ b/client/coral-framework/hocs/withResendEmailConfirmation.js @@ -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 ( + + ); + } + } + + return WithResendEmailConfirmaton; +}); diff --git a/client/coral-framework/hocs/withSetUsername.js b/client/coral-framework/hocs/withSetUsername.js new file mode 100644 index 000000000..05323efa1 --- /dev/null +++ b/client/coral-framework/hocs/withSetUsername.js @@ -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 ( + + ); + } + } + + 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 +); diff --git a/client/coral-framework/hocs/withSignIn.js b/client/coral-framework/hocs/withSignIn.js new file mode 100644 index 000000000..d377d8ada --- /dev/null +++ b/client/coral-framework/hocs/withSignIn.js @@ -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 ( + + ); + } + } + + return WithSignIn; +}); diff --git a/client/coral-framework/hocs/withSignUp.js b/client/coral-framework/hocs/withSignUp.js new file mode 100644 index 000000000..8c35e1cc9 --- /dev/null +++ b/client/coral-framework/hocs/withSignUp.js @@ -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 ( + + ); + } + } + + return WithSignUp; +}); + +export default compose(withSettingsQuery, withSignUp); diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js new file mode 100644 index 000000000..3af606d0b --- /dev/null +++ b/client/coral-framework/reducers/auth.js @@ -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; + } +} diff --git a/client/coral-admin/src/reducers/config.js b/client/coral-framework/reducers/config.js similarity index 54% rename from client/coral-admin/src/reducers/config.js rename to client/coral-framework/reducers/config.js index 0588f0a13..a3a0409eb 100644 --- a/client/coral-admin/src/reducers/config.js +++ b/client/coral-framework/reducers/config.js @@ -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; diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js new file mode 100644 index 000000000..3661ebaef --- /dev/null +++ b/client/coral-framework/reducers/index.js @@ -0,0 +1,8 @@ +import auth from './auth'; +import config from './config'; + +export default { + auth, + login: auth, + config, +}; diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index 6e0f69915..a7dd6df98 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -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. diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index 8f9fa8080..6f99a2291 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -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 ( diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index d53b9b496..66792b9b3 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -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(); +} diff --git a/client/coral-login/src/containers/Main.js b/client/coral-login/src/containers/Main.js new file mode 100644 index 000000000..1d38b9b8e --- /dev/null +++ b/client/coral-login/src/containers/Main.js @@ -0,0 +1,10 @@ +import React from 'react'; +import Slot from 'coral-framework/components/Slot'; + +class Main extends React.Component { + render() { + return ; + } +} + +export default Main; diff --git a/client/coral-login/src/index.js b/client/coral-login/src/index.js new file mode 100644 index 000000000..aa81bce18 --- /dev/null +++ b/client/coral-login/src/index.js @@ -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( + +
+ , + document.querySelector('#talk-login-container') + ); +} + +main(); diff --git a/client/coral-ui/components/BareButton.css b/client/coral-ui/components/BareButton.css new file mode 100644 index 000000000..0a9c81625 --- /dev/null +++ b/client/coral-ui/components/BareButton.css @@ -0,0 +1,3 @@ +.bare { + composes: buttonReset from "coral-framework/styles/reset.css"; +} diff --git a/client/coral-ui/components/BareButton.js b/client/coral-ui/components/BareButton.js new file mode 100644 index 000000000..6d3f9fa93 --- /dev/null +++ b/client/coral-ui/components/BareButton.js @@ -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 ; +}; + +BareButton.propTypes = { + className: PropTypes.string, + anchor: PropTypes.bool, +}; + +export default BareButton; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 7ab267b0f..38c6a8650 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -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'; diff --git a/docs/_docs/02-01-required-configuration.md b/docs/_docs/02-01-required-configuration.md index da235ed90..4d3cd5dbd 100644 --- a/docs/_docs/02-01-required-configuration.md +++ b/docs/_docs/02-01-required-configuration.md @@ -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. diff --git a/docs/_docs/02-02-advanced-configuration.md b/docs/_docs/02-02-advanced-configuration.md index e985f8f20..d7881987a 100644 --- a/docs/_docs/02-02-advanced-configuration.md +++ b/docs/_docs/02-02-advanced-configuration.md @@ -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,4 @@ 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.** \ No newline at end of file +**Note: Apollo Engine is a premium service, charges may apply.** diff --git a/locales/en.yml b/locales/en.yml index 1494ae28a..8f7a3853b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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)" diff --git a/locales/es.yml b/locales/es.yml index 76599de73..28e12ac3f 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -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" diff --git a/locales/fr.yml b/locales/fr.yml index 50d9dedb7..a922d931d 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -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" diff --git a/locales/zh_CN.yml b/locales/zh_CN.yml index 83f9cb7c2..3108addf6 100644 --- a/locales/zh_CN.yml +++ b/locales/zh_CN.yml @@ -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: "举报理由(可选)" diff --git a/locales/zh_TW.yml b/locales/zh_TW.yml index d515f5c56..c6698fec4 100644 --- a/locales/zh_TW.yml +++ b/locales/zh_TW.yml @@ -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: "舉報原因(可選)" diff --git a/plugin-api/beta/client/actions/auth.js b/plugin-api/beta/client/actions/auth.js new file mode 100644 index 000000000..85e2bd844 --- /dev/null +++ b/plugin-api/beta/client/actions/auth.js @@ -0,0 +1,5 @@ +export { + setAuthToken, + handleSuccessfulLogin, + logout, +} from 'coral-framework/actions/auth'; diff --git a/plugin-api/beta/client/actions/stream.js b/plugin-api/beta/client/actions/stream.js index 2751f4a17..bb3d465d9 100644 --- a/plugin-api/beta/client/actions/stream.js +++ b/plugin-api/beta/client/actions/stream.js @@ -1 +1,2 @@ export { setSort } from 'coral-embed-stream/src/actions/stream'; +export { showSignInDialog } from 'coral-embed-stream/src/actions/login'; diff --git a/plugin-api/beta/client/components/index.js b/plugin-api/beta/client/components/index.js index 974c4609f..fc6159544 100644 --- a/plugin-api/beta/client/components/index.js +++ b/plugin-api/beta/client/components/index.js @@ -26,3 +26,4 @@ export { export { default as StreamConfiguration, } from 'coral-framework/components/StreamConfiguration'; +export { default as Recaptcha } from 'coral-framework/components/Recaptcha'; diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js index 0d7cac460..397d8b94d 100644 --- a/plugin-api/beta/client/hocs/index.js +++ b/plugin-api/beta/client/hocs/index.js @@ -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, diff --git a/plugin-api/beta/client/hocs/withReaction.js b/plugin-api/beta/client/hocs/withReaction.js index 6020d4002..95d3f5bcf 100644 --- a/plugin-api/beta/client/hocs/withReaction.js +++ b/plugin-api/beta/client/hocs/withReaction.js @@ -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 diff --git a/plugin-api/beta/client/selectors/auth.js b/plugin-api/beta/client/selectors/auth.js new file mode 100644 index 000000000..a82e11aab --- /dev/null +++ b/plugin-api/beta/client/selectors/auth.js @@ -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'); diff --git a/plugins.default.json b/plugins.default.json index e6d1b23cb..066c94478 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -1,7 +1,6 @@ { "server": [ "talk-plugin-auth", - "talk-plugin-facebook-auth", "talk-plugin-featured-comments", "talk-plugin-offtopic", "talk-plugin-respect" diff --git a/plugins/talk-plugin-auth/client/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/components/ChangeUsername.js deleted file mode 100644 index e814a9d05..000000000 --- a/plugins/talk-plugin-auth/client/components/ChangeUsername.js +++ /dev/null @@ -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 ( -
- -
- ); - } -} - -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); diff --git a/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js b/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js deleted file mode 100644 index 08ddc171e..000000000 --- a/plugins/talk-plugin-auth/client/components/CreateUsernameDialog.js +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './styles.css'; -import { - Dialog, - Alert, - TextField, - Button, -} from 'plugin-api/beta/client/components/ui'; -import { FakeComment } from './FakeComment'; -import t from 'coral-framework/services/i18n'; - -const CreateUsernameDialog = ({ - open, - handleClose, - formData, - handleSubmitUsername, - handleChange, - ...props -}) => ( - - - × - -
-
-

{t('createdisplay.write_your_username')}

-
-
-

- {t('createdisplay.your_username')} -

- -

- {t('createdisplay.if_you_dont_change_your_name')} -

- {props.auth.error && {props.auth.error}} -
- {props.errors.username && ( - - {' '} - {t('createdisplay.special_characters')}{' '} - - )} -
- - -
-
-
-
-
-); - -CreateUsernameDialog.propTypes = { - open: PropTypes.bool, - handleClose: PropTypes.func, - formData: PropTypes.object, - handleSubmitUsername: PropTypes.func, - handleChange: PropTypes.func, - auth: PropTypes.object, - errors: PropTypes.object, -}; - -export default CreateUsernameDialog; diff --git a/plugins/talk-plugin-auth/client/components/ForgotContent.js b/plugins/talk-plugin-auth/client/components/ForgotContent.js deleted file mode 100644 index bfadbb001..000000000 --- a/plugins/talk-plugin-auth/client/components/ForgotContent.js +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './styles.css'; -import { Button, TextField } from 'plugin-api/beta/client/components/ui'; -import t from 'coral-framework/services/i18n'; - -class ForgotContent extends React.Component { - state = { value: '' }; - - handleSubmit = e => { - e.preventDefault(); - this.props.fetchForgotPassword(this.state.value); - }; - - handleChangeEmail = e => { - const { value } = e.target; - this.setState({ value }); - }; - - render() { - const { changeView, auth } = this.props; - const { passwordRequestSuccess, passwordRequestFailure } = auth; - - return ( -
-
-

{t('sign_in.recover_password')}

-
-
-
- -
- - {passwordRequestSuccess ? ( -

- {passwordRequestSuccess} -

- ) : null} - {passwordRequestFailure ? ( -

- {passwordRequestFailure} -

- ) : null} -
-
- - {t('sign_in.need_an_account')}{' '} - changeView('SIGNUP')}>{t('sign_in.register')} - - - {t('sign_in.already_have_an_account')}{' '} - changeView('SIGNIN')}>{t('sign_in.sign_in')} - -
-
- ); - } -} - -ForgotContent.propTypes = { - auth: PropTypes.object, - changeView: PropTypes.func, - fetchForgotPassword: PropTypes.func, -}; - -export default ForgotContent; diff --git a/plugins/talk-plugin-auth/client/components/ResendVerification.js b/plugins/talk-plugin-auth/client/components/ResendVerification.js deleted file mode 100644 index d2d71dd45..000000000 --- a/plugins/talk-plugin-auth/client/components/ResendVerification.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { - Button, - Spinner, - Success, - Alert, -} from 'plugin-api/beta/client/components/ui'; -import PropTypes from 'prop-types'; -import styles from './ResendVerification.css'; -import t from 'coral-framework/services/i18n'; - -class ResendVerification extends React.Component { - render() { - const { resendVerification, error, loading, success, email } = this.props; - return ( -
-

{t('sign_in.email_verify_cta')}

- - {error && ( - - {error.translation_key - ? t(`error.${error.translation_key}`) - : error.toString()} - - )} -
- {t('error.email_not_verified', email)} -
-
- - {loading && } - {success && } -
-
- ); - } -} - -ResendVerification.propTypes = { - resendVerification: PropTypes.bool.isRequired, - error: PropTypes.object, - loading: PropTypes.bool, - success: PropTypes.bool, - email: PropTypes.string.isRequired, -}; - -export default ResendVerification; diff --git a/plugins/talk-plugin-auth/client/components/SignDialog.js b/plugins/talk-plugin-auth/client/components/SignDialog.js deleted file mode 100644 index a33baeb99..000000000 --- a/plugins/talk-plugin-auth/client/components/SignDialog.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Dialog } from 'plugin-api/beta/client/components/ui'; -import styles from './styles.css'; - -import SignInContent from './SignInContent'; -import SignUpContent from './SignUpContent'; -import ForgotContent from './ForgotContent'; -import ResendVerification from './ResendVerification'; - -const SignDialog = ({ open, view, resetSignInDialog, ...props }) => ( - - {view !== 'SIGNIN' && ( - - × - - )} - {view === 'SIGNIN' && } - {view === 'SIGNUP' && } - {view === 'FORGOT' && } - {view === 'RESEND_VERIFICATION' && ( - - )} - -); - -export default SignDialog; diff --git a/plugins/talk-plugin-auth/client/components/SignInButton.js b/plugins/talk-plugin-auth/client/components/SignInButton.js deleted file mode 100644 index 163a5e5f8..000000000 --- a/plugins/talk-plugin-auth/client/components/SignInButton.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Button } from 'plugin-api/beta/client/components/ui'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { showSignInDialog } from 'coral-embed-stream/src/actions/auth'; -import t from 'coral-framework/services/i18n'; - -const SignInButton = ({ loggedIn, showSignInDialog }) => ( -
- {!loggedIn ? ( - - ) : null} -
-); - -const mapStateToProps = ({ auth }) => ({ - loggedIn: auth.loggedIn, -}); - -const mapDispatchToProps = dispatch => - bindActionCreators({ showSignInDialog }, dispatch); - -export default connect(mapStateToProps, mapDispatchToProps)(SignInButton); diff --git a/plugins/talk-plugin-auth/client/components/SignInContainer.js b/plugins/talk-plugin-auth/client/components/SignInContainer.js deleted file mode 100644 index 6f99f9ce6..000000000 --- a/plugins/talk-plugin-auth/client/components/SignInContainer.js +++ /dev/null @@ -1,202 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import SignDialog from './SignDialog'; -import { bindActionCreators } from 'redux'; -import t from 'coral-framework/services/i18n'; -import errorMsj from 'coral-framework/helpers/error'; -import validate from 'coral-framework/helpers/validate'; - -import { - changeView, - fetchSignUp, - fetchSignIn, - hideSignInDialog, - fetchSignInFacebook, - fetchSignUpFacebook, - fetchForgotPassword, - requestConfirmEmail, - resetSignInDialog, - facebookCallback, - invalidForm, - validForm, -} from 'coral-embed-stream/src/actions/auth'; - -class SignInContainer extends React.Component { - constructor(props) { - super(props); - - this.state = { - formData: { - email: '', - username: '', - password: '', - confirmPassword: '', - }, - errors: {}, - showErrors: false, - }; - } - - componentDidMount() { - this.listenToStorageChanges(); - const { formData } = this.state; - const errors = Object.keys(formData).reduce((map, prop) => { - map[prop] = t('sign_in.required_field'); - return map; - }, {}); - this.setState({ errors }); - } - - componentWillUnmount() { - this.unlisten(); - } - - listenToStorageChanges() { - window.addEventListener('storage', this.handleAuth); - } - - unlisten() { - window.removeEventListener('storage', this.handleAuth); - } - - handleAuth = e => { - // Listening to FB changes - // FB localStorage key is 'auth' - const authCallback = this.props.facebookCallback; - - if (e.key === 'auth') { - const { err, data } = JSON.parse(e.newValue); - authCallback(err, data); - this.unlisten(); - localStorage.removeItem('auth'); - } - }; - - handleChange = e => { - const { name, value } = e.target; - this.setState( - state => ({ - ...state, - formData: { - ...state.formData, - [name]: value, - }, - }), - () => { - this.validation(name, value); - } - ); - }; - - resendVerification = () => { - this.props.requestConfirmEmail(this.props.auth.email).then(() => { - setTimeout(() => { - // allow success UI to be shown for a second, and then close the modal - this.props.resetSignInDialog(); - }, 2500); - }); - }; - - addError = (name, error) => { - return this.setState(state => ({ - errors: { - ...state.errors, - [name]: error, - }, - })); - }; - - validation = (name, value) => { - const { addError } = this; - const { formData } = this.state; - - if (!value.length) { - addError(name, t('sign_in.required_field')); - } else if ( - name === 'confirmPassword' && - formData.confirmPassword !== formData.password - ) { - addError('confirmPassword', t('sign_in.passwords_dont_match')); - } 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 }); - }; - - handleSignUp = e => { - e.preventDefault(); - const { errors } = this.state; - const { fetchSignUp, validForm, invalidForm } = this.props; - this.displayErrors(); - if (this.isCompleted() && !Object.keys(errors).length) { - fetchSignUp(this.state.formData); - validForm(); - } else { - invalidForm(t('sign_in.check_the_form')); - } - }; - - handleSignIn = e => { - e.preventDefault(); - this.props.fetchSignIn(this.state.formData); - }; - - render() { - const { auth } = this.props; - const { - requireEmailConfirmation, - emailVerificationLoading, - emailVerificationSuccess, - } = auth; - - return ( - - ); - } -} - -const mapStateToProps = state => ({ - auth: state.auth, -}); - -const mapDispatchToProps = dispatch => - bindActionCreators( - { - facebookCallback, - fetchSignUp, - fetchSignIn, - fetchSignInFacebook, - fetchSignUpFacebook, - fetchForgotPassword, - requestConfirmEmail, - changeView, - hideSignInDialog, - resetSignInDialog, - invalidForm, - validForm, - }, - dispatch - ); - -export default connect(mapStateToProps, mapDispatchToProps)(SignInContainer); diff --git a/plugins/talk-plugin-auth/client/components/SignInContent.js b/plugins/talk-plugin-auth/client/components/SignInContent.js deleted file mode 100644 index e74de4ff2..000000000 --- a/plugins/talk-plugin-auth/client/components/SignInContent.js +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - TextField, - Spinner, - Alert, -} from 'plugin-api/beta/client/components/ui'; -import styles from './styles.css'; -import t from 'coral-framework/services/i18n'; - -const SignInContent = ({ - handleChange, - formData, - changeView, - handleSignIn, - auth, - fetchSignInFacebook, -}) => { - return ( -
-
-

{t('sign_in.sign_in_to_join')}

-
- {auth.error && ( - - {auth.error.translation_key - ? t(`error.${auth.error.translation_key}`) - : auth.error.toString()} - - )} -
-
- -
-
-

{t('sign_in.or')}

-
-
- - -
- {!auth.isLoading ? ( - - ) : ( - - )} -
- -
- -
- ); -}; - -SignInContent.propTypes = { - auth: PropTypes.shape({ - isLoading: PropTypes.bool.isRequired, - error: PropTypes.string, - emailVerificationFailure: PropTypes.bool, - }).isRequired, - fetchSignInFacebook: PropTypes.func.isRequired, - handleSignIn: PropTypes.func.isRequired, - handleChange: PropTypes.func.isRequired, - changeView: PropTypes.func.isRequired, - emailVerificationLoading: PropTypes.bool.isRequired, - emailVerificationSuccess: PropTypes.bool.isRequired, - resendVerification: PropTypes.func.isRequired, - formData: PropTypes.object, -}; - -export default SignInContent; diff --git a/plugins/talk-plugin-auth/client/components/SignUpContent.js b/plugins/talk-plugin-auth/client/components/SignUpContent.js deleted file mode 100644 index 845fe502c..000000000 --- a/plugins/talk-plugin-auth/client/components/SignUpContent.js +++ /dev/null @@ -1,143 +0,0 @@ -import styles from './styles.css'; -import React from 'react'; -import { - Button, - TextField, - Spinner, - Success, - Alert, -} from 'plugin-api/beta/client/components/ui'; -import t from 'coral-framework/services/i18n'; - -class SignUpContent extends React.Component { - componentWillReceiveProps(next) { - if ( - !this.props.emailVerificationEnabled && - !this.props.auth.successSignUp && - next.auth.successSignUp - ) { - setTimeout(() => { - this.props.changeView('SIGNIN'); - }, 2000); - } - } - - render() { - const { - handleChange, - formData, - emailVerificationEnabled, - auth, - errors, - showErrors, - changeView, - handleSignUp, - fetchSignUpFacebook, - } = this.props; - - return ( -
-
-

{t('sign_in.sign_up')}

-
- - {auth.error && {auth.error}} - {!auth.successSignUp && ( -
-
- -
-
-

{t('sign_in.or')}

-
-
- - - - {errors.password && ( - - {' '} - Password must be at least 8 characters.{' '} - - )} - -
- - {auth.isLoading && } -
- -
- )} - {auth.successSignUp && ( -
- - {emailVerificationEnabled && ( -

- {t('sign_in.verify_email')} -
-
- {t('sign_in.verify_email2')} -

- )} -
- )} -
- {t('sign_in.already_have_an_account')}{' '} - changeView('SIGNIN')}> - {t('sign_in.sign_in')} - -
-
- ); - } -} - -export default SignUpContent; diff --git a/plugins/talk-plugin-auth/client/components/UserBox.js b/plugins/talk-plugin-auth/client/components/UserBox.js deleted file mode 100644 index d601ace74..000000000 --- a/plugins/talk-plugin-auth/client/components/UserBox.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import styles from './styles.css'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import t from 'coral-framework/services/i18n'; -import { logout } from 'coral-embed-stream/src/actions/auth'; - -const UserBox = ({ loggedIn, user, logout, onShowProfile }) => ( -
- {loggedIn ? ( -
- - {t('sign_in.logged_in_as')} - - {user.username}. {t('sign_in.not_you')} - logout()} - > - {t('sign_in.logout')} - -
- ) : null} -
-); - -const mapStateToProps = ({ auth }) => ({ - loggedIn: auth.loggedIn, - user: auth.user, -}); - -const mapDispatchToProps = dispatch => bindActionCreators({ logout }, dispatch); - -export default connect(mapStateToProps, mapDispatchToProps)(UserBox); diff --git a/plugins/talk-plugin-auth/client/components/styles.css b/plugins/talk-plugin-auth/client/components/styles.css deleted file mode 100644 index 735f34d31..000000000 --- a/plugins/talk-plugin-auth/client/components/styles.css +++ /dev/null @@ -1,167 +0,0 @@ -.dialog { - border: none; - box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); - width: 280px; - top: 10px; -} - -.header { - margin-bottom: 20px; -} - -.header h1, .separator h1{ - text-align: center; - font-size: 1.2em; -} - -.footer { - margin: 20px auto 10px; - text-align: center; -} - -.footer span { - display: block; - margin-bottom: 5px; -} - -.footer a { - color: #2c69b6; - cursor: pointer; - margin: 0 5px; -} - -.socialConnections { - margin-bottom: 20px; -} - -.signInButton { - margin-top: 10px; - background-color: #2a2a2a; -} - -.close { - font-size: 20px; - line-height: 14px; - top: 10px; - right: 10px; - position: absolute; - display: block; - font-weight: bold; - color: #363636; - cursor: pointer; -} - -.close:hover { - color: #6b6b6b; -} - -input.error{ - border: solid 2px #f44336; -} - -.errorMsg, .hint { - color: grey; - font-weight: 600; - padding: 3px 0 16px; -} - -.userBox { - margin: 10px 0 20px; - letter-spacing: 0.1px; -} - -.userBoxLoggedIn { - font-weight: bold; -} - -.userBox a { - color: black; - font-weight: bold; - cursor: pointer; - margin: 0px; - margin-left: 4px; - padding-bottom: 2px; -} - -.userBox .logout { - border-bottom: solid 1px black; -} - -.attention { - display: inline-block; - width: 15px; - height: 15px; - background: #B71C1C; - color: #FFEBEE; - font-weight: bolder; - padding: 4px; - vertical-align: middle; - border-radius: 20px; - box-sizing: border-box; - font-size: 9px; - line-height: 7px; - text-align: center; - margin-right: 5px; -} - -.action { - margin-top: 0px; -} - -.passwordRequestSuccess { - border: 1px solid green; - background-color: lightgreen; - padding: 10px; -} - -.passwordRequestFailure { - border: 1px solid orange; - background-color: 1px solid coral; - padding: 10px; -} - -.emailConfirmDialog { - margin-top: 15px; -} - -.confirmLabel { - display: block; -} - -/* Change username Dialog*/ - -.dialogusername { - border: none; - box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); - width: 400px; - top: 10px; -} - -.yourusername { - display: block; -} - -.example { - display: block; -} - -.ifyoudont { - display: block; - margin-top: 15px; -} - -.saveusername { - display: block; - width: 100%; -} - -.savebutton { - display: inline; - background-color: rgb(105,105,105); - color: white; -} - -.fakeComment { - display: block; - margin-bottom: 5px; -} diff --git a/plugins/talk-plugin-auth/client/index.js b/plugins/talk-plugin-auth/client/index.js index 12aa8ccd5..13abce1d9 100644 --- a/plugins/talk-plugin-auth/client/index.js +++ b/plugins/talk-plugin-auth/client/index.js @@ -1,13 +1,15 @@ -import UserBox from './components/UserBox'; -import SignInButton from './components/SignInButton'; -import SignInContainer from './components/SignInContainer'; -import ChangeUserNameContainer from './components/ChangeUsername'; +import UserBox from './stream/containers/UserBox'; +import SignInButton from './stream/containers/SignInButton'; +import SetUsernameDialog from './stream/containers/SetUsernameDialog'; import translations from './translations.yml'; +import Login from './login/containers/Main'; +import reducer from './login/reducer'; export default { + reducer, translations, slots: { - stream: [UserBox, SignInButton, ChangeUserNameContainer], - login: [SignInContainer], + stream: [UserBox, SignInButton, SetUsernameDialog], + login: [Login], }, }; diff --git a/plugins/talk-plugin-auth/client/login/actions.js b/plugins/talk-plugin-auth/client/login/actions.js new file mode 100644 index 000000000..593556507 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/actions.js @@ -0,0 +1,16 @@ +import * as actions from './constants'; + +export const setView = view => ({ + type: actions.SET_VIEW, + view, +}); + +export const setEmail = email => ({ + type: actions.SET_EMAIL, + email, +}); + +export const setPassword = password => ({ + type: actions.SET_PASSWORD, + password, +}); diff --git a/plugins/talk-plugin-auth/client/login/components/External.css b/plugins/talk-plugin-auth/client/login/components/External.css new file mode 100644 index 000000000..8fe36394a --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/External.css @@ -0,0 +1,16 @@ +.external { + margin-bottom: 20px; +} + +.separator h1{ + text-align: center; + font-size: 1.2em; +} + +.slot > * { + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0px; + } +} diff --git a/plugins/talk-plugin-auth/client/login/components/External.js b/plugins/talk-plugin-auth/client/login/components/External.js new file mode 100644 index 000000000..8f3c16e59 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/External.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './External.css'; +import { Slot } from 'plugin-api/beta/client/components'; +import { IfSlotIsNotEmpty } from 'plugin-api/beta/client/components'; +import { t } from 'plugin-api/beta/client/services'; + +const External = ({ slot }) => ( + +
+
+ +
+
+

{t('talk-plugin-auth.login.or')}

+
+
+
+); + +External.propTypes = { + slot: PropTypes.string.isRequired, +}; + +export default External; diff --git a/plugins/talk-plugin-auth/client/login/components/ForgotPassword.css b/plugins/talk-plugin-auth/client/login/components/ForgotPassword.css new file mode 100644 index 000000000..ca00726c4 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/ForgotPassword.css @@ -0,0 +1,42 @@ +.header { + margin-bottom: 20px; +} + +.header h1, .separator h1{ + text-align: center; + font-size: 1.2em; +} + +.button { + margin-top: 10px; + background-color: #2a2a2a; +} +.footer { + margin: 20px auto 10px; + text-align: center; +} + +.footer span { + display: block; + margin-bottom: 5px; +} + +.footer a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; +} + +.success { + border: 1px solid green; + background-color: lightgreen; + padding: 10px; +} + +.failure { + border: 1px solid orange; + background-color: 1px solid coral; + padding: 10px; +} + + diff --git a/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js b/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js new file mode 100644 index 000000000..8fbd48c64 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './ForgotPassword.css'; +import { Button, TextField } from 'plugin-api/beta/client/components/ui'; +import { t } from 'plugin-api/beta/client/services'; + +class ForgotPassword extends React.Component { + handleSignUpLink = e => { + e.preventDefault(); + this.props.onSignUpLink(); + }; + handleSignInLink = e => { + e.preventDefault(); + this.props.onSignInLink(); + }; + handleEmailChange = e => this.props.onEmailChange(e.target.value); + handleSubmit = e => { + e.preventDefault(); + this.props.onSubmit(); + }; + + render() { + const { email, errorMessage, success } = this.props; + + return ( +
+
+

{t('talk-plugin-auth.login.recover_password')}

+
+
+
+ +
+ + {success ? ( +

{t('password_reset.mail_sent')}

+ ) : null} + {errorMessage ? ( +

{errorMessage}

+ ) : null} +
+
+ + {t('talk-plugin-auth.login.need_an_account')}{' '} + + {t('talk-plugin-auth.login.register')} + + + + {t('talk-plugin-auth.login.already_have_an_account')}{' '} + + {t('talk-plugin-auth.login.sign_in')} + + +
+
+ ); + } +} + +ForgotPassword.propTypes = { + success: PropTypes.bool.isRequired, + email: PropTypes.string.isRequired, + onEmailChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + onSignInLink: PropTypes.func.isRequired, + onSignUpLink: PropTypes.func.isRequired, +}; + +export default ForgotPassword; diff --git a/plugins/talk-plugin-auth/client/login/components/Main.css b/plugins/talk-plugin-auth/client/login/components/Main.css new file mode 100644 index 000000000..eb48d61c1 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/Main.css @@ -0,0 +1,24 @@ +.dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 280px; + top: 10px; + font-family: Helvetica, 'Helvetica Neue', Verdana, sans-serif; + font-size: 14px; +} + +.close { + font-size: 20px; + line-height: 14px; + top: 10px; + right: 10px; + position: absolute; + display: block; + font-weight: bold; + color: #363636; + cursor: pointer; +} + +.close:hover { + color: #6b6b6b; +} diff --git a/plugins/talk-plugin-auth/client/login/components/Main.js b/plugins/talk-plugin-auth/client/login/components/Main.js new file mode 100644 index 000000000..992d7a2fd --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/Main.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { Dialog } from 'plugin-api/beta/client/components/ui'; +import styles from './Main.css'; +import PropTypes from 'prop-types'; +import SignIn from '../containers/SignIn'; +import SignUp from '../containers/SignUp'; +import ForgotPassword from '../containers/ForgotPassword'; +import ResendEmailConfirmation from '../containers/ResendEmailConfirmation'; +import * as views from '../enums/views'; + +const Main = ({ view, onResetView }) => ( + + {view !== views.SIGN_IN && ( + + × + + )} + {view === views.SIGN_IN && } + {view === views.SIGN_UP && } + {view === views.FORGOT_PASSWORD && } + {view === views.RESEND_EMAIL_CONFIRMATION && } + +); + +Main.propTypes = { + view: PropTypes.string.isRequired, + onResetView: PropTypes.func.isRequired, +}; + +export default Main; diff --git a/plugins/talk-plugin-auth/client/components/ResendVerification.css b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.css similarity index 100% rename from plugins/talk-plugin-auth/client/components/ResendVerification.css rename to plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.css diff --git a/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.js b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.js new file mode 100644 index 000000000..6b316838d --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { + Button, + Spinner, + Success, + Alert, +} from 'plugin-api/beta/client/components/ui'; +import { t } from 'plugin-api/beta/client/services'; +import PropTypes from 'prop-types'; +import styles from './ResendEmailConfirmation.css'; + +class ResendVerification extends React.Component { + handleSubmit = e => { + e.preventDefault(); + this.props.onSubmit(); + }; + + render() { + const { email, errorMessage, loading, success } = this.props; + return ( +
+

+ {t('talk-plugin-auth.login.email_verify_cta')} +

+ + {errorMessage && {errorMessage}} +
+ {t('error.email_not_verified', email)} +
+
+ {!loading && + !success && ( + + )} + {loading && } + {success && } +
+
+ ); + } +} + +ResendVerification.propTypes = { + success: PropTypes.bool.isRequired, + loading: PropTypes.bool.isRequired, + email: PropTypes.string.isRequired, + onSubmit: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, +}; + +export default ResendVerification; diff --git a/plugins/talk-plugin-auth/client/login/components/SignIn.css b/plugins/talk-plugin-auth/client/login/components/SignIn.css new file mode 100644 index 000000000..6b9c9de85 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/SignIn.css @@ -0,0 +1,38 @@ +.header { + margin-bottom: 20px; +} + +.header h1 { + text-align: center; + font-size: 1.2em; +} + +.action { + margin-top: 0px; +} + +.signInButton { + margin-top: 10px; + background-color: #2a2a2a; +} + +.footer { + margin: 20px auto 10px; + text-align: center; +} + +.footer span { + display: block; + margin-bottom: 5px; +} + +.footer a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; +} + +.recaptcha { + margin-top: 16px; + margin-bottom: 16px; +} diff --git a/plugins/talk-plugin-auth/client/login/components/SignIn.js b/plugins/talk-plugin-auth/client/login/components/SignIn.js new file mode 100644 index 000000000..333ca0b0b --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/SignIn.js @@ -0,0 +1,136 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Button, + TextField, + Spinner, + Alert, +} from 'plugin-api/beta/client/components/ui'; +import styles from './SignIn.css'; +import { t } from 'plugin-api/beta/client/services'; +import cn from 'classnames'; +import { Recaptcha } from 'plugin-api/beta/client/components'; +import External from './External'; + +class SignIn extends React.Component { + recaptcha = null; + + handleForgotPasswordLink = e => { + e.preventDefault(); + this.props.onForgotPasswordLink(); + }; + handleSignUpLink = e => { + e.preventDefault(); + this.props.onSignUpLink(); + }; + 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, + loading, + } = this.props; + return ( +
+
+

{t('talk-plugin-auth.login.sign_in_to_join')}

+
+ {errorMessage && {errorMessage}} +
+ +
+ + + {requireRecaptcha && ( +
+ +
+ )} +
+ {!loading ? ( + + ) : ( + + )} +
+ +
+ +
+ ); + } +} + +SignIn.propTypes = { + loading: PropTypes.bool.isRequired, + email: PropTypes.string.isRequired, + password: PropTypes.string.isRequired, + onEmailChange: PropTypes.func.isRequired, + onPasswordChange: PropTypes.func.isRequired, + onForgotPasswordLink: PropTypes.func.isRequired, + onSignUpLink: PropTypes.func.isRequired, + onRecaptchaVerify: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + requireRecaptcha: PropTypes.bool.isRequired, +}; + +export default SignIn; diff --git a/plugins/talk-plugin-auth/client/login/components/SignUp.css b/plugins/talk-plugin-auth/client/login/components/SignUp.css new file mode 100644 index 000000000..6d5474e57 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/SignUp.css @@ -0,0 +1,40 @@ +.header { + margin-bottom: 20px; +} + +.header h1 { + text-align: center; + font-size: 1.2em; +} + +.hint { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} + +.action { + margin-top: 0px; +} + +.button { + margin-top: 10px; + background-color: #2a2a2a; +} + +.footer { + margin: 20px auto 10px; + text-align: center; +} + +.footer span { + display: block; + margin-bottom: 5px; +} + +.footer a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; +} + diff --git a/plugins/talk-plugin-auth/client/login/components/SignUp.js b/plugins/talk-plugin-auth/client/login/components/SignUp.js new file mode 100644 index 000000000..601a7dba0 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/components/SignUp.js @@ -0,0 +1,166 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Button, + TextField, + Spinner, + Success, + Alert, +} from 'plugin-api/beta/client/components/ui'; +import { t } from 'plugin-api/beta/client/services'; +import styles from './SignUp.css'; +import External from './External'; + +class SignUp extends React.Component { + handleSignInLink = e => { + e.preventDefault(); + this.props.onSignInLink(); + }; + + handleUsernameChange = e => this.props.onUsernameChange(e.target.value); + handleEmailChange = e => this.props.onEmailChange(e.target.value); + handlePasswordChange = e => this.props.onPasswordChange(e.target.value); + handlePasswordRepeatChange = e => + this.props.onPasswordRepeatChange(e.target.value); + + handleSubmit = e => { + e.preventDefault(); + this.props.onSubmit(); + }; + + render() { + const { + username, + email, + password, + passwordRepeat, + usernameError, + emailError, + passwordError, + passwordRepeatError, + loading, + errorMessage, + requireEmailConfirmation, + success, + } = this.props; + + return ( +
+
+

{t('talk-plugin-auth.login.sign_up')}

+
+ + {errorMessage && {errorMessage}} + {!success && ( +
+ +
+ + + + {passwordError && ( + + {' '} + Password must be at least 8 characters.{' '} + + )} + +
+ + {loading && } +
+ +
+ )} + {success && ( +
+ + {requireEmailConfirmation && ( +

+ {t('talk-plugin-auth.login.verify_email')} +
+
+ {t('talk-plugin-auth.login.verify_email2')} +

+ )} +
+ )} +
+ {t('talk-plugin-auth.login.already_have_an_account')}{' '} + + {t('talk-plugin-auth.login.sign_in')} + +
+
+ ); + } +} + +SignUp.propTypes = { + loading: PropTypes.bool.isRequired, + username: PropTypes.string.isRequired, + usernameError: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + emailError: PropTypes.string.isRequired, + password: PropTypes.string.isRequired, + passwordError: PropTypes.string.isRequired, + passwordRepeat: PropTypes.string.isRequired, + passwordRepeatError: PropTypes.string.isRequired, + onUsernameChange: PropTypes.func.isRequired, + onEmailChange: PropTypes.func.isRequired, + onPasswordChange: PropTypes.func.isRequired, + onPasswordRepeatChange: PropTypes.func.isRequired, + onSignInLink: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + requireEmailConfirmation: PropTypes.bool.isRequired, + success: PropTypes.bool.isRequired, +}; + +export default SignUp; diff --git a/plugins/talk-plugin-auth/client/login/constants.js b/plugins/talk-plugin-auth/client/login/constants.js new file mode 100644 index 000000000..3997c11f4 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/constants.js @@ -0,0 +1,5 @@ +const prefix = 'TALK_AUTH'; + +export const SET_VIEW = `${prefix}_SET_VIEW`; +export const SET_EMAIL = `${prefix}_SET_EMAIL`; +export const SET_PASSWORD = `${prefix}_SET_PASSWORD`; diff --git a/plugins/talk-plugin-auth/client/login/containers/ForgotPassword.js b/plugins/talk-plugin-auth/client/login/containers/ForgotPassword.js new file mode 100644 index 000000000..9b1f6cb48 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/containers/ForgotPassword.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect, withForgotPassword } from 'plugin-api/beta/client/hocs'; +import { compose } from 'recompose'; +import ForgotPassword from '../components/ForgotPassword'; +import { bindActionCreators } from 'redux'; +import * as views from '../enums/views'; +import { setView, setEmail } from '../actions'; + +class ForgotPasswordContainer extends Component { + handleSubmit = () => { + this.props.forgotPassword(this.props.email); + }; + + handleSignUpLink = () => { + this.props.setView(views.SIGN_UP); + }; + + handleSignInLink = () => { + this.props.setView(views.SIGN_IN); + }; + + render() { + return ( + + ); + } +} + +ForgotPasswordContainer.propTypes = { + success: PropTypes.bool.isRequired, + forgotPassword: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + setView: PropTypes.func.isRequired, + email: PropTypes.string.isRequired, + setEmail: PropTypes.func.isRequired, +}; + +const mapStateToProps = ({ talkPluginAuth: state }) => ({ + email: state.email, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + setView, + setEmail, + }, + dispatch + ); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withForgotPassword +)(ForgotPasswordContainer); diff --git a/plugins/talk-plugin-auth/client/login/containers/Main.js b/plugins/talk-plugin-auth/client/login/containers/Main.js new file mode 100644 index 000000000..05778133a --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/containers/Main.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Main from '../components/Main'; +import { connect } from 'plugin-api/beta/client/hocs'; +import { bindActionCreators } from 'redux'; +import { setView } from '../actions'; +import { + setAuthToken, + handleSuccessfulLogin, +} from 'plugin-api/beta/client/actions/auth'; +import * as views from '../enums/views'; + +class MainContainer extends React.Component { + resetView = () => { + this.props.setView(views.SIGN_IN); + }; + + resizeHeight() { + setTimeout(() => { + const height = document.getElementById('signInDialog').offsetHeight + 100; + window.resizeTo(500, height); + }, 20); + } + + componentDidMount() { + this.resizeHeight(); + this.listenToStorageChanges(); + } + + componentDidUpdate(prevProps) { + if (prevProps.view !== this.props.view) { + this.resizeHeight(); + } + } + + componentWillUnmount() { + this.unlisten(); + } + + listenToStorageChanges() { + window.addEventListener('storage', this.handleAuth); + } + + unlisten() { + window.removeEventListener('storage', this.handleAuth); + } + + // External logins store auth data into `auth`, we use it to detect + // a successful sign in. + handleAuth = e => { + if (e.key === 'auth') { + const { err, data } = JSON.parse(e.newValue); + if (err) { + console.error(err); + } else if (data && data.token) { + if (data.user) { + this.props.handleSuccessfulLogin(data.user, data.token); + } else { + this.props.setAuthToken(data.token); + } + this.unlisten(); + localStorage.removeItem('auth'); + window.close(); + } else { + console.error('auth was set, but did not contain a token'); + } + } + }; + + render() { + return
; + } +} + +MainContainer.propTypes = { + view: PropTypes.string.isRequired, + setView: PropTypes.func.isRequired, + handleSuccessfulLogin: PropTypes.func.isRequired, + setAuthToken: PropTypes.func.isRequired, +}; + +const mapStateToProps = ({ talkPluginAuth: state }) => ({ + view: state.view, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + setView, + handleSuccessfulLogin, + setAuthToken, + }, + dispatch + ); + +export default connect(mapStateToProps, mapDispatchToProps)(MainContainer); diff --git a/plugins/talk-plugin-auth/client/login/containers/ResendEmailConfirmation.js b/plugins/talk-plugin-auth/client/login/containers/ResendEmailConfirmation.js new file mode 100644 index 000000000..bd7c9ad8c --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/containers/ResendEmailConfirmation.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + connect, + withResendEmailConfirmation, +} from 'plugin-api/beta/client/hocs'; +import { compose } from 'recompose'; +import ResendEmailConfirmaton from '../components/ResendEmailConfirmation'; +import { bindActionCreators } from 'redux'; +import * as views from '../enums/views'; +import { setView } from '../actions'; + +class ResendEmailConfirmatonContainer extends Component { + handleSubmit = () => { + this.props.resendEmailConfirmation(this.props.email); + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.success) { + setTimeout(() => { + // allow success UI to be shown for a second, and then close the modal + this.props.setView(views.SIGN_IN); + }, 2500); + } + } + + render() { + return ( + + ); + } +} + +ResendEmailConfirmatonContainer.propTypes = { + success: PropTypes.bool.isRequired, + loading: PropTypes.bool.isRequired, + resendEmailConfirmation: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + setView: PropTypes.func.isRequired, + email: PropTypes.string.isRequired, +}; + +const mapStateToProps = ({ talkPluginAuth: state }) => ({ + email: state.email, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + setView, + }, + dispatch + ); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withResendEmailConfirmation +)(ResendEmailConfirmatonContainer); diff --git a/plugins/talk-plugin-auth/client/login/containers/SignIn.js b/plugins/talk-plugin-auth/client/login/containers/SignIn.js new file mode 100644 index 000000000..ed797aed3 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/containers/SignIn.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect, withSignIn } from 'plugin-api/beta/client/hocs'; +import { compose } from 'recompose'; +import SignIn from '../components/SignIn'; +import { bindActionCreators } from 'redux'; +import * as views from '../enums/views'; +import { setView, setEmail, setPassword } from '../actions'; + +class SignInContainer extends Component { + state = { + recaptchaResponse: null, + }; + + handleSubmit = () => { + this.props.signIn( + this.props.email, + this.props.password, + this.state.recaptchaResponse + ); + }; + + handleRecaptchaVerify = recaptchaResponse => { + this.setState({ recaptchaResponse }); + }; + + handleForgotPasswordLink = () => { + this.props.setView(views.FORGOT_PASSWORD); + }; + + handleSignUpLink = () => { + this.props.setView(views.SIGN_UP); + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.requireEmailConfirmation) { + this.props.setView(views.RESEND_EMAIL_CONFIRMATION); + } else if (nextProps.success) { + window.close(); + } + } + + render() { + return ( + + ); + } +} + +SignInContainer.propTypes = { + signIn: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, + requireRecaptcha: PropTypes.bool.isRequired, + requireEmailConfirmation: PropTypes.bool.isRequired, + loading: PropTypes.bool.isRequired, + success: PropTypes.bool.isRequired, + setView: PropTypes.func.isRequired, + email: PropTypes.string.isRequired, + password: PropTypes.string.isRequired, + setEmail: PropTypes.func.isRequired, + setPassword: PropTypes.func.isRequired, +}; + +const mapStateToProps = ({ talkPluginAuth: state }) => ({ + email: state.email, + password: state.password, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + setView, + setEmail, + setPassword, + }, + dispatch + ); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withSignIn +)(SignInContainer); diff --git a/plugins/talk-plugin-auth/client/login/containers/SignUp.js b/plugins/talk-plugin-auth/client/login/containers/SignUp.js new file mode 100644 index 000000000..cadca6f86 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/containers/SignUp.js @@ -0,0 +1,136 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect, withSignUp } from 'plugin-api/beta/client/hocs'; +import { compose } from 'recompose'; +import SignUp from '../components/SignUp'; +import { bindActionCreators } from 'redux'; +import * as views from '../enums/views'; +import { setView, setEmail, setPassword } from '../actions'; +import { t } from 'plugin-api/beta/client/services'; + +class SignUpContainer extends Component { + state = { + username: '', + passwordRepeat: '', + usernameError: null, + emailError: null, + passwordError: null, + passwordRepeatError: null, + }; + + validate = data => { + let valid = true; + const changes = {}; + Object.keys(data).forEach(name => { + const error = this.props.validate(name, data[name]); + if (error) { + valid = false; + } + changes[`${name}Error`] = error; + }); + + if (data.password !== data.passwordRepeat) { + changes['passwordRepeatError'] = t( + 'talk-plugin-auth.login.passwords_dont_match' + ); + valid = false; + } + + this.setState(changes); + return valid; + }; + + handleSubmit = () => { + const data = { + username: this.state.username, + email: this.props.email, + password: this.props.password, + passwordRepeat: this.state.passwordRepeat, + }; + + if (this.validate(data)) { + this.props.signUp(data); + } + }; + + setUsername = username => this.setState({ username }); + setPasswordRepeat = passwordRepeat => this.setState({ passwordRepeat }); + + handleForgotPasswordLink = () => { + this.props.setView(views.FORGOT_PASSWORD); + }; + + handleSignInLink = () => { + this.props.setView(views.SIGN_IN); + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.success) { + setTimeout(() => { + // allow success UI to be shown for a second, and then close the modal + this.props.setView(views.SIGN_IN); + }, 2000); + } + } + + render() { + return ( + + ); + } +} + +SignUpContainer.propTypes = { + setView: PropTypes.func.isRequired, + email: PropTypes.string.isRequired, + password: PropTypes.string.isRequired, + setEmail: PropTypes.func.isRequired, + setPassword: PropTypes.func.isRequired, + signUp: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, + errorMessage: PropTypes.string.isRequired, + requireEmailConfirmation: PropTypes.bool.isRequired, + success: PropTypes.bool.isRequired, + validate: PropTypes.func.isRequired, +}; + +const mapStateToProps = ({ talkPluginAuth: state }) => ({ + email: state.email, + password: state.password, +}); + +const mapDispatchToProps = dispatch => + bindActionCreators( + { + setView, + setEmail, + setPassword, + }, + dispatch + ); + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withSignUp +)(SignUpContainer); diff --git a/plugins/talk-plugin-auth/client/login/enums/views.js b/plugins/talk-plugin-auth/client/login/enums/views.js new file mode 100644 index 000000000..5c6732755 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/enums/views.js @@ -0,0 +1,4 @@ +export const SIGN_IN = 'SIGN_IN'; +export const FORGOT_PASSWORD = 'FORGOT_PASSWORD'; +export const SIGN_UP = 'SIGN_UP'; +export const RESEND_EMAIL_CONFIRMATION = 'RESEND_EMAIL_CONFIRMATION'; diff --git a/plugins/talk-plugin-auth/client/login/reducer.js b/plugins/talk-plugin-auth/client/login/reducer.js new file mode 100644 index 000000000..004aff8f8 --- /dev/null +++ b/plugins/talk-plugin-auth/client/login/reducer.js @@ -0,0 +1,30 @@ +import * as actions from './constants'; +import * as views from './enums/views'; + +const initialState = { + view: views.SIGN_IN, + email: '', + password: '', +}; + +export default function reducer(state = initialState, action) { + switch (action.type) { + case actions.SET_VIEW: + return { + ...state, + view: action.view, + }; + case actions.SET_EMAIL: + return { + ...state, + email: action.email, + }; + case actions.SET_PASSWORD: + return { + ...state, + password: action.password, + }; + default: + return state; + } +} diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.css b/plugins/talk-plugin-auth/client/stream/components/FakeComment.css similarity index 100% rename from plugins/talk-plugin-auth/client/components/FakeComment.css rename to plugins/talk-plugin-auth/client/stream/components/FakeComment.css diff --git a/plugins/talk-plugin-auth/client/components/FakeComment.js b/plugins/talk-plugin-auth/client/stream/components/FakeComment.js similarity index 72% rename from plugins/talk-plugin-auth/client/components/FakeComment.js rename to plugins/talk-plugin-auth/client/stream/components/FakeComment.js index 82c61182b..e353a7f58 100644 --- a/plugins/talk-plugin-auth/client/components/FakeComment.js +++ b/plugins/talk-plugin-auth/client/stream/components/FakeComment.js @@ -1,9 +1,9 @@ import React from 'react'; -import t from 'coral-framework/services/i18n'; -import ReplyButton from 'coral-embed-stream/src/tabs/stream/components/ReplyButton'; +import PropTypes from 'prop-types'; import styles from './FakeComment.css'; import { Icon } from 'plugin-api/beta/client/components/ui'; import { CommentTimestamp } from 'plugin-api/beta/client/components'; +import { t } from 'plugin-api/beta/client/services'; export const FakeComment = ({ username, created_at, body }) => (
@@ -16,11 +16,10 @@ export const FakeComment = ({ username, created_at, body }) => ( {t('like')} - {}} - parentCommentId={'commentID'} - currentUserId={{}} - /> +
); + +FakeComment.propTypes = { + username: PropTypes.string.isRequired, + created_at: PropTypes.string.isRequired, + body: PropTypes.string.isRequired, +}; diff --git a/plugins/talk-plugin-auth/client/stream/components/SetUsernameDialog.css b/plugins/talk-plugin-auth/client/stream/components/SetUsernameDialog.css new file mode 100644 index 000000000..c1947ca7d --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/components/SetUsernameDialog.css @@ -0,0 +1,41 @@ +.dialogusername { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 400px; + top: 10px; +} + +.yourusername { + display: block; +} + +.header { + margin-bottom: 20px; +} + +.header h1 { + text-align: center; + font-size: 1.2em; +} + +.saveusername { + display: block; + width: 100%; +} + +.savebutton { + display: inline; + background-color: rgb(105,105,105); + color: white; +} + +.fakeComment { + display: block; + margin-bottom: 5px; +} + +.hint { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} diff --git a/plugins/talk-plugin-auth/client/stream/components/SetUsernameDialog.js b/plugins/talk-plugin-auth/client/stream/components/SetUsernameDialog.js new file mode 100644 index 000000000..d9517a06f --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/components/SetUsernameDialog.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './SetUsernameDialog.css'; +import { + Dialog, + Alert, + TextField, + Button, +} from 'plugin-api/beta/client/components/ui'; +import { FakeComment } from './FakeComment'; +import { t } from 'plugin-api/beta/client/services'; + +class SetUsernameDialog extends React.Component { + handleUsernameChange = e => this.props.onUsernameChange(e.target.value); + + handleSubmit = e => { + e.preventDefault(); + this.props.onSubmit(); + }; + + render() { + const { username, usernameError, errorMessage } = this.props; + + return ( + +
+
+

+ {t('talk-plugin-auth.set_username_dialog.write_your_username')} +

+
+
+

+ {t('talk-plugin-auth.set_username_dialog.your_username')} +

+ + {errorMessage && {errorMessage}} +
+ {usernameError && ( + + {' '} + {t( + 'talk-plugin-auth.set_username_dialog.special_characters' + )}{' '} + + )} +
+ + +
+
+
+
+
+ ); + } +} + +SetUsernameDialog.propTypes = { + loading: PropTypes.bool.isRequired, + username: PropTypes.string.isRequired, + usernameError: PropTypes.string.isRequired, + onUsernameChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + errorMessage: PropTypes.string.isRequired, +}; + +export default SetUsernameDialog; diff --git a/plugins/talk-plugin-auth/client/stream/components/SignInButton.css b/plugins/talk-plugin-auth/client/stream/components/SignInButton.css new file mode 100644 index 000000000..774c6f280 --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/components/SignInButton.css @@ -0,0 +1,9 @@ +.button { + background-color: #2a2a2a; + color: #FFF; +} + +.button:hover { + background-color: #767676; + color: #FFF; +} diff --git a/plugins/talk-plugin-auth/client/stream/components/SignInButton.js b/plugins/talk-plugin-auth/client/stream/components/SignInButton.js new file mode 100644 index 000000000..53e0bc23d --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/components/SignInButton.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'plugin-api/beta/client/components/ui'; +import { t } from 'plugin-api/beta/client/services'; +import styles from './SignInButton.css'; + +const SignInButton = ({ isLoggedIn, showSignInDialog }) => ( +
+ {!isLoggedIn ? ( + + ) : null} +
+); + +SignInButton.propTypes = { + isLoggedIn: PropTypes.bool, + showSignInDialog: PropTypes.func, +}; + +export default SignInButton; diff --git a/plugins/talk-plugin-auth/client/stream/components/UserBox.css b/plugins/talk-plugin-auth/client/stream/components/UserBox.css new file mode 100644 index 000000000..799468177 --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/components/UserBox.css @@ -0,0 +1,21 @@ +.userBox { + margin: 10px 0 20px; + letter-spacing: 0.1px; +} + +.userBoxLoggedIn { + font-weight: bold; +} + +.userBox a { + color: black; + font-weight: bold; + cursor: pointer; + margin: 0px; + margin-left: 4px; + padding-bottom: 2px; +} + +.userBox .logout { + border-bottom: solid 1px black; +} diff --git a/plugins/talk-plugin-auth/client/stream/components/UserBox.js b/plugins/talk-plugin-auth/client/stream/components/UserBox.js new file mode 100644 index 000000000..aa9d14df6 --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/components/UserBox.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './UserBox.css'; +import { t } from 'plugin-api/beta/client/services'; +import cn from 'classnames'; + +const UserBox = ({ username, logout, onShowProfile }) => ( +
+ {username ? ( +
+ + {t('talk-plugin-auth.login.logged_in_as')} + + {username}.{' '} + {t('talk-plugin-auth.login.not_you')} + logout()} + > + {t('talk-plugin-auth.login.logout')} + +
+ ) : null} +
+); + +UserBox.propTypes = { + username: PropTypes.string, + logout: PropTypes.func, + onShowProfile: PropTypes.func, +}; + +export default UserBox; diff --git a/plugins/talk-plugin-auth/client/stream/containers/SetUsernameDialog.js b/plugins/talk-plugin-auth/client/stream/containers/SetUsernameDialog.js new file mode 100644 index 000000000..ae89b9fac --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/containers/SetUsernameDialog.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect, withSetUsername } from 'plugin-api/beta/client/hocs'; +import { compose, branch, renderNothing } from 'recompose'; +import { + usernameStatusSelector, + usernameSelector, +} from 'plugin-api/beta/client/selectors/auth'; +import SetUsernameDialog from '../components/SetUsernameDialog'; + +class SetUsernameDialogContainer extends Component { + state = { + username: this.props.username, + usernameError: null, + }; + + handleSubmit = () => { + const validationError = this.props.validateUsername(this.state.username); + if (validationError) { + this.setState({ usernameError: validationError }); + } else { + this.props.setUsername(this.state.username); + } + }; + + setUsername = username => this.setState({ username }); + + render() { + if (!this.props.unset) { + return null; + } + return ( + + ); + } +} + +SetUsernameDialogContainer.propTypes = { + unset: PropTypes.bool.isRequired, + username: PropTypes.string, + setUsername: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, + errorMessage: PropTypes.string.isRequired, + success: PropTypes.bool.isRequired, + validateUsername: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => ({ + unset: usernameStatusSelector(state) === 'UNSET', + username: usernameSelector(state), +}); + +export default compose( + connect(mapStateToProps, null), + withSetUsername, + branch(props => !props.username, renderNothing) +)(SetUsernameDialogContainer); diff --git a/plugins/talk-plugin-auth/client/stream/containers/SignInButton.js b/plugins/talk-plugin-auth/client/stream/containers/SignInButton.js new file mode 100644 index 000000000..87b2a900f --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/containers/SignInButton.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { showSignInDialog } from 'plugin-api/beta/client/actions/stream'; +import SignInButton from '../components/SignInButton'; +import { isLoggedInSelector } from 'plugin-api/beta/client/selectors/auth'; + +const mapStateToProps = state => ({ + isLoggedIn: isLoggedInSelector(state), +}); + +const mapDispatchToProps = dispatch => + bindActionCreators({ showSignInDialog }, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(SignInButton); diff --git a/plugins/talk-plugin-auth/client/stream/containers/UserBox.js b/plugins/talk-plugin-auth/client/stream/containers/UserBox.js new file mode 100644 index 000000000..720a61087 --- /dev/null +++ b/plugins/talk-plugin-auth/client/stream/containers/UserBox.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { logout } from 'plugin-api/beta/client/actions/auth'; +import UserBox from '../components/UserBox'; +import { usernameSelector } from 'plugin-api/beta/client/selectors/auth'; + +const mapStateToProps = state => ({ + username: usernameSelector(state), +}); + +const mapDispatchToProps = dispatch => bindActionCreators({ logout }, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(UserBox); diff --git a/plugins/talk-plugin-auth/client/translations.yml b/plugins/talk-plugin-auth/client/translations.yml index bdc9761fa..9731da90a 100644 --- a/plugins/talk-plugin-auth/client/translations.yml +++ b/plugins/talk-plugin-auth/client/translations.yml @@ -1,226 +1,225 @@ da: - sign_in: - email_verify_cta: "Please verify your email address." - request_new_verify_email: "Request another email" - verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." - verify_email2: "You must verify your account before engaging with the community." - not_you: "Not you?" - logged_in_as: "Signed in as" - facebook_sign_in: "Sign in with Facebook" - facebook_sign_up: "Sign up with Facebook" - logout: "Sign out" - sign_in: "Sign in" - sign_in_to_join: "Sign in to join the conversation" - or: "Or" - email: "E-mail Address" - password: "Password" - forgot_your_pass: "Forgot your password?" - need_an_account: "Need an account?" - register: "Register" - sign_up: "Sign Up" - confirm_password: "Confirm Password" - username: "Username" - already_have_an_account: "Already have an account?" - recover_password: "Recover password" - email_in_use: "Email address already in use" - email_or_username_in_use: "Email address or Username already in use" - required_field: "This field is required" - passwords_dont_match: "Passwords don't match." - special_characters: "Usernames can contain letters, numbers and _ only" - sign_in_to_comment: "Sign in to comment" - check_the_form: "Invalid Form. Please, check the fields" - createdisplay: - check_the_form: "Invalid Form. Please check the fields" - continue: "Continue with the same Facebook username" - error_create: "Error when changing username" - fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section." - fake_comment_date: "1 minute ago" - if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments." - required_field: "Required field" - save: Save - special_characters: "Usernames can contain letters numbers and _ only" - username: Username - write_your_username: "Edit your username" - your_username: "Your username appears on every comment you post." + talk-plugin-auth: + login: + email_verify_cta: "Please verify your email address." + request_new_verify_email: "Request another email" + verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." + verify_email2: "You must verify your account before engaging with the community." + not_you: "Not you?" + logged_in_as: "Signed in as" + facebook_sign_in: "Sign in with Facebook" + facebook_sign_up: "Sign up with Facebook" + logout: "Sign out" + sign_in: "Sign in" + sign_in_to_join: "Sign in to join the conversation" + or: "Or" + email: "E-mail Address" + password: "Password" + forgot_your_pass: "Forgot your password?" + need_an_account: "Need an account?" + register: "Register" + sign_up: "Sign Up" + confirm_password: "Confirm Password" + username: "Username" + already_have_an_account: "Already have an account?" + recover_password: "Recover password" + email_in_use: "Email address already in use" + email_or_username_in_use: "Email address or Username already in use" + required_field: "This field is required" + passwords_dont_match: "Passwords don't match." + special_characters: "Usernames can contain letters, numbers and _ only" + sign_in_to_comment: "Sign in to comment" + check_the_form: "Invalid Form. Please, check the fields" + set_username_dialog: + check_the_form: "Invalid Form. Please check the fields" + continue: "Continue with the same Facebook username" + error_create: "Error when changing username" + fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section." + fake_comment_date: "1 minute ago" + if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments." + required_field: "Required field" + save: Save + special_characters: "Usernames can contain letters numbers and _ only" + username: Username + write_your_username: "Edit your username" + your_username: "Your username appears on every comment you post." en: - sign_in: - email_verify_cta: "Please verify your email address." - request_new_verify_email: "Request another email" - verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." - verify_email2: "You must verify your account before engaging with the community." - not_you: "Not you?" - logged_in_as: "Signed in as" - facebook_sign_in: "Sign in with Facebook" - facebook_sign_up: "Sign up with Facebook" - logout: "Sign out" - sign_in: "Sign in" - sign_in_to_join: "Sign in to join the conversation" - or: "Or" - email: "E-mail Address" - password: "Password" - forgot_your_pass: "Forgot your password?" - need_an_account: "Need an account?" - register: "Register" - sign_up: "Sign Up" - confirm_password: "Confirm Password" - username: "Username" - already_have_an_account: "Already have an account?" - recover_password: "Recover password" - email_in_use: "Email address already in use" - email_or_username_in_use: "Email address or Username already in use" - required_field: "This field is required" - passwords_dont_match: "Passwords don't match." - special_characters: "Usernames can contain letters, numbers and _ only" - sign_in_to_comment: "Sign in to comment" - check_the_form: "Invalid Form. Please, check the fields" - createdisplay: - check_the_form: "Invalid Form. Please check the fields" - continue: "Continue with the same Facebook username" - error_create: "Error when changing username" - fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section." - fake_comment_date: "1 minute ago" - if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments." - required_field: "Required field" - save: Save - special_characters: "Usernames can contain letters numbers and _ only" - username: Username - write_your_username: "Edit your username" - your_username: "Your username appears on every comment you post." + talk-plugin-auth: + login: + email_verify_cta: "Please verify your email address." + request_new_verify_email: "Request another email" + verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." + verify_email2: "You must verify your account before engaging with the community." + not_you: "Not you?" + logged_in_as: "Signed in as" + logout: "Sign out" + sign_in: "Sign in" + sign_in_to_join: "Sign in to join the conversation" + or: "Or" + email: "E-mail Address" + password: "Password" + forgot_your_pass: "Forgot your password?" + need_an_account: "Need an account?" + register: "Register" + sign_up: "Sign Up" + confirm_password: "Confirm Password" + username: "Username" + already_have_an_account: "Already have an account?" + recover_password: "Recover password" + email_in_use: "Email address already in use" + email_or_username_in_use: "Email address or Username already in use" + required_field: "This field is required" + passwords_dont_match: "Passwords don't match." + special_characters: "Usernames can contain letters, numbers and _ only" + sign_in_to_comment: "Sign in to comment" + check_the_form: "Invalid Form. Please, check the fields" + set_username_dialog: + check_the_form: "Invalid Form. Please check the fields" + continue: "Continue with the same Facebook username" + error_create: "Error when changing username" + fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section." + fake_comment_date: "1 minute ago" + if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments." + required_field: "Required field" + save: Save + special_characters: "Usernames can contain letters numbers and _ only" + username: Username + write_your_username: "Edit your username" + your_username: "Your username appears on every comment you post." de: - sign_in: - email_verify_cta: "Bitte bestätigen Sie Ihre E-Mail-Adresse." - request_new_verify_email: "E-Mail-Bestätigung erneut anfordern" - verify_email: "Danke, dass Sie ein Konto eingerichtet haben! Wir haben eine Nachricht an Ihre E-Mail-Adresse geschickt, mit der Sie Ihr Konto bestätigen können." - verify_email2: "Sie müssen ihr Konto bestätigen, bevor Sie beginnen können." - not_you: "Nicht Ihr Name?" - logged_in_as: "Angemeldet als" - facebook_sign_in: "Mit Facebook anmelden" - facebook_sign_up: "Mit Facebook registrieren" - logout: "Abmelden" - sign_in: "Anmelden" - sign_in_to_join: "Anmelden, um zu diskutieren" - or: "Oder" - email: "E-Mail-Adresse" - password: "Passwort" - forgot_your_pass: "Passwort vergessen?" - need_an_account: "Brauchen Sie ein Konto?" - register: "Registrieren" - sign_up: "Registrieren" - confirm_password: "Passwort bestätigen" - username: "Nutzername" - already_have_an_account: "Konto bereits vorhanden?" - recover_password: "Passwort zurücksetzen" - email_in_use: "E-Mail-Adresse bereits vergeben" - email_or_username_in_use: "E-Mail-Adresse oder Nutzername bereits vergeben" - required_field: "Pflichtfeld" - passwords_dont_match: "Passwörter nicht identisch." - special_characters: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten" - sign_in_to_comment: "Zum Kommentieren anmelden" - check_the_form: "Ungültige Eingabe. Bitte prüfen Sie Ihre Felder" - createdisplay: - check_the_form: "Ungültige Eingabe. Bitte prüfen Sie Ihre Felder" - continue: "Mit diesem Facebook-Profilnamen fortfahren" - error_create: "Fehler bei Änderung des Nutzernamens" - fake_comment_body: "Dies ist ein Beispiel-Kommentar. Leser können im Kommentarbereich Meinungen und Gedanken mit der Redaktion austauschen." - fake_comment_date: "Vor 1 Minute" - if_you_dont_change_your_name: "Wenn Sie Ihren Nutzernamen an diesem Punkt nicht ändern, wird Ihr Facebook-Name mit Ihren Kommentaren angezeigt." - required_field: "Pflichtfeld" - save: Speichern - special_characters: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten" - username: Nutzername - write_your_username: "Nutzername bearbeiten" - your_username: "Ihr Nutzername erscheint an jedem Ihrer veröffentlichten Kommentare." + talk-plugin-auth: + login: + email_verify_cta: "Bitte bestätigen Sie Ihre E-Mail-Adresse." + request_new_verify_email: "E-Mail-Bestätigung erneut anfordern" + verify_email: "Danke, dass Sie ein Konto eingerichtet haben! Wir haben eine Nachricht an Ihre E-Mail-Adresse geschickt, mit der Sie Ihr Konto bestätigen können." + verify_email2: "Sie müssen ihr Konto bestätigen, bevor Sie beginnen können." + not_you: "Nicht Ihr Name?" + logged_in_as: "Angemeldet als" + facebook_sign_in: "Mit Facebook anmelden" + facebook_sign_up: "Mit Facebook registrieren" + logout: "Abmelden" + sign_in: "Anmelden" + sign_in_to_join: "Anmelden, um zu diskutieren" + or: "Oder" + email: "E-Mail-Adresse" + password: "Passwort" + forgot_your_pass: "Passwort vergessen?" + need_an_account: "Brauchen Sie ein Konto?" + register: "Registrieren" + sign_up: "Registrieren" + confirm_password: "Passwort bestätigen" + username: "Nutzername" + already_have_an_account: "Konto bereits vorhanden?" + recover_password: "Passwort zurücksetzen" + email_in_use: "E-Mail-Adresse bereits vergeben" + email_or_username_in_use: "E-Mail-Adresse oder Nutzername bereits vergeben" + required_field: "Pflichtfeld" + passwords_dont_match: "Passwörter nicht identisch." + special_characters: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten" + sign_in_to_comment: "Zum Kommentieren anmelden" + check_the_form: "Ungültige Eingabe. Bitte prüfen Sie Ihre Felder" + set_username_dialog: + check_the_form: "Ungültige Eingabe. Bitte prüfen Sie Ihre Felder" + continue: "Mit diesem Facebook-Profilnamen fortfahren" + error_create: "Fehler bei Änderung des Nutzernamens" + fake_comment_body: "Dies ist ein Beispiel-Kommentar. Leser können im Kommentarbereich Meinungen und Gedanken mit der Redaktion austauschen." + fake_comment_date: "Vor 1 Minute" + if_you_dont_change_your_name: "Wenn Sie Ihren Nutzernamen an diesem Punkt nicht ändern, wird Ihr Facebook-Name mit Ihren Kommentaren angezeigt." + required_field: "Pflichtfeld" + save: Speichern + special_characters: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten" + username: Nutzername + write_your_username: "Nutzername bearbeiten" + your_username: "Ihr Nutzername erscheint an jedem Ihrer veröffentlichten Kommentare." es: - sign_in: - email_verify_cta: "Por favor confirme su correo." - request_new_verify_email: "Enviar otro correo" - verify_email: "¡Gracias por crear una cuenta! Le enviamos un correo a la direcció\ - n que dio para confirmar su cuenta." - verify_email2: "Debe confirmarla antes de poder involucrarse en la comunidad." - not_you: "¿No eres tu?" - logged_in_as: "Entraste como" - facebook_sign_in: "Entrar con Facebook" - facebook_sign_up: "Registrarse con Facebook" - logout: "Salir" - sign_in: "Entrar" - sign_in_to_join: "Entrar para unirte a la conversación" - or: "O" - email: "Dirección de Correo" - password: "Contraseña" - forgot_your_pass: "¿Has olvidado tu contraseña?" - need_an_account: "¿Necesitas una cuenta?" - register: "Registrar" - sign_up: "Registro" - confirm_password: "Confirmar Contraseña" - username: "Nombre" - already_have_an_account: "¿Ya tienes una cuenta?" - recover_password: "Recuperar la contraseña" - email_in_use: "Este correo se encuentra en uso" - email_or_username_in_use: "Este correo ó nombre de usuario se encuentran en uso" - required_field: "Este campo es requerido" - passwords_dont_match: "Las contraseñas no coinciden." - special_characters: "Los nombres pueden contener letras, números y _" - sign_in_to_comment: "Entrar para comentar" - check_the_form: "Formulario Inválido. Por favor, completa los campos" - createdisplay: - check_the_form: "Formulario Inválido. Por favor verifica los campos" - continue: "Continuar con el mismo nombre que aparece en Facebook" - error_create: "Hubo un error al cambiar el nombre de usuario" - fake_comment_body: "Este es un comentario de ejemplo. Las lectoras pueden compartir\ - \ sus ideas y opiniones con los periodistas en la sección de comentarios." - fake_comment_date: "hace un minuto" - if_you_dont_change_your_name: "Si no modificas tu nombre de usuario en este paso\ - \ tu nombre de Facebook aparecerá al lado de cada comentario que publiques." - required_field: "Campo requerido" - save: Guardar - special_characters: "Los nombres sólo pueden contener letras números y _" - username: Nombre - write_your_username: "Edita tu nombre" - your_username: "Tu nombre aparece en cada comentario que publiques." + talk-plugin-auth: + login: + email_verify_cta: "Por favor confirme su correo." + request_new_verify_email: "Enviar otro correo" + verify_email: "¡Gracias por crear una cuenta! Le enviamos un correo a la direcció\ + n que dio para confirmar su cuenta." + verify_email2: "Debe confirmarla antes de poder involucrarse en la comunidad." + not_you: "¿No eres tu?" + logged_in_as: "Entraste como" + logout: "Salir" + sign_in: "Entrar" + sign_in_to_join: "Entrar para unirte a la conversación" + or: "O" + email: "Dirección de Correo" + password: "Contraseña" + forgot_your_pass: "¿Has olvidado tu contraseña?" + need_an_account: "¿Necesitas una cuenta?" + register: "Registrar" + sign_up: "Registro" + confirm_password: "Confirmar Contraseña" + username: "Nombre" + already_have_an_account: "¿Ya tienes una cuenta?" + recover_password: "Recuperar la contraseña" + email_in_use: "Este correo se encuentra en uso" + email_or_username_in_use: "Este correo ó nombre de usuario se encuentran en uso" + required_field: "Este campo es requerido" + passwords_dont_match: "Las contraseñas no coinciden." + special_characters: "Los nombres pueden contener letras, números y _" + sign_in_to_comment: "Entrar para comentar" + check_the_form: "Formulario Inválido. Por favor, completa los campos" + set_username_dialog: + check_the_form: "Formulario Inválido. Por favor verifica los campos" + continue: "Continuar con el mismo nombre que aparece en Facebook" + error_create: "Hubo un error al cambiar el nombre de usuario" + fake_comment_body: "Este es un comentario de ejemplo. Las lectoras pueden compartir\ + \ sus ideas y opiniones con los periodistas en la sección de comentarios." + fake_comment_date: "hace un minuto" + if_you_dont_change_your_name: "Si no modificas tu nombre de usuario en este paso\ + \ tu nombre de Facebook aparecerá al lado de cada comentario que publiques." + required_field: "Campo requerido" + save: Guardar + special_characters: "Los nombres sólo pueden contener letras números y _" + username: Nombre + write_your_username: "Edita tu nombre" + your_username: "Tu nombre aparece en cada comentario que publiques." fr: - sign_in: - email_verify_cta: "Merci de vérifier votre adresse e-mail." - request_new_verify_email: "Demander un nouvel envoi d'e-mail" - verify_email: "Merci d'avoir créé un compte ! Nous avons envoyé un courrier électronique à l'adresse que vous avez fournie pour vérifier votre adresse e-mail." - verify_email2: "Vous devez vérifier votre adresse e-mail avant de vous engager auprès de la communauté." - not_you: "Pas vous ?" - logged_in_as: "Connecté en tant que" - facebook_sign_in: "Connectez-vous avec Facebook" - facebook_sign_up: "Inscrivez-vous avec Facebook" - logout: "Se déconnecter" - sign_in: "Se connecter" - sign_in_to_join: "Connectez-vous pour participer à la conversation" - or: "Ou" - email: "Adresse e-mail" - password: "Mot de passe" - forgot_your_pass: "Mot de passe oublié ?" - need_an_account: "Besoin d'un compte ?" - register: "Inscription" - sign_up: "S'inscrire" - confirm_password: "Confirmez Le mot de passe" - username: "Nom d'utilisateur" - already_have_an_account: "Vous avez déjà un compte ?" - recover_password: "Récupérer le mot de passe" - email_in_use: "L'adresse e-mail est déjà utilisée" - email_or_username_in_use: "Adresse e-mail ou nom d'utilisateur déjà utilisé" - required_field: "Ce champ est obligatoire" - passwords_dont_match: "Les mots de passe ne correspondent pas." - special_characters: "Les noms d'utilisateur ne peuvent contenir que des lettres, des chiffres et \"_\" seulement" - sign_in_to_comment: "Connectez-vous pour commenter" - check_the_form: "Formulaire non valide. Veuillez vérifier les champs" - createdisplay: - check_the_form: "Formulaire non valide. Veuillez vérifier les champs" - continue: "Continuez avec le même nom d'utilisateur que sur Facebook" - error_create: "Erreur lors de la modification du nom d'utilisateur" - fake_comment_body: "Ceci est un exemple de commentaire. Les lecteurs peuvent partager leurs pensées et opinions avec les rédactions dans la section des commentaires." - fake_comment_date: "Il y a 1 minute" - if_you_dont_change_your_name: "Si vous ne modifiez pas votre nom d'utilisateur à cette étape, votre nom d'affichage Facebook apparaîtra à côté de tous vos commentaires." - required_field: "champs requis" - save: Sauvegarder - special_characters: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" - username: "Nom d'utilisateur" - write_your_username: "Modifier votre nom d'utilisateur" - your_username: "Votre nom d'utilisateur apparaît sur chaque commentaire que vous publiez." + talk-plugin-auth: + login: + email_verify_cta: "Merci de vérifier votre adresse e-mail." + request_new_verify_email: "Demander un nouvel envoi d'e-mail" + verify_email: "Merci d'avoir créé un compte ! Nous avons envoyé un courrier électronique à l'adresse que vous avez fournie pour vérifier votre adresse e-mail." + verify_email2: "Vous devez vérifier votre adresse e-mail avant de vous engager auprès de la communauté." + not_you: "Pas vous ?" + logged_in_as: "Connecté en tant que" + logout: "Se déconnecter" + sign_in: "Se connecter" + sign_in_to_join: "Connectez-vous pour participer à la conversation" + or: "Ou" + email: "Adresse e-mail" + password: "Mot de passe" + forgot_your_pass: "Mot de passe oublié ?" + need_an_account: "Besoin d'un compte ?" + register: "Inscription" + sign_up: "S'inscrire" + confirm_password: "Confirmez Le mot de passe" + username: "Nom d'utilisateur" + already_have_an_account: "Vous avez déjà un compte ?" + recover_password: "Récupérer le mot de passe" + email_in_use: "L'adresse e-mail est déjà utilisée" + email_or_username_in_use: "Adresse e-mail ou nom d'utilisateur déjà utilisé" + required_field: "Ce champ est obligatoire" + passwords_dont_match: "Les mots de passe ne correspondent pas." + special_characters: "Les noms d'utilisateur ne peuvent contenir que des lettres, des chiffres et \"_\" seulement" + sign_in_to_comment: "Connectez-vous pour commenter" + check_the_form: "Formulaire non valide. Veuillez vérifier les champs" + set_username_dialog: + check_the_form: "Formulaire non valide. Veuillez vérifier les champs" + continue: "Continuez avec le même nom d'utilisateur que sur Facebook" + error_create: "Erreur lors de la modification du nom d'utilisateur" + fake_comment_body: "Ceci est un exemple de commentaire. Les lecteurs peuvent partager leurs pensées et opinions avec les rédactions dans la section des commentaires." + fake_comment_date: "Il y a 1 minute" + if_you_dont_change_your_name: "Si vous ne modifiez pas votre nom d'utilisateur à cette étape, votre nom d'affichage Facebook apparaîtra à côté de tous vos commentaires." + required_field: "champs requis" + save: Sauvegarder + special_characters: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\"" + username: "Nom d'utilisateur" + write_your_username: "Modifier votre nom d'utilisateur" + your_username: "Votre nom d'utilisateur apparaît sur chaque commentaire que vous publiez." nl_NL: sign_in: email_verify_cta: "Controleer je e-mailadres." @@ -266,134 +265,133 @@ nl_NL: write_your_username: "Wijzig je gebruikersnaam" your_username: "Je gebruikersnaam verschijnt bij al je reacties." pt_BR: - sign_in: - email_verify_cta: "Please verify your email address." - request_new_verify_email: "Request another email" - verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." - verify_email2: "You must verify your account before engaging with the community." - not_you: "Not you?" - logged_in_as: "Signed in as" - facebook_sign_in: "Sign in with Facebook" - facebook_sign_up: "Sign up with Facebook" - logout: "Sign out" - sign_in: "Sign in" - sign_in_to_join: "Sign in to join the conversation" - or: "Or" - email: "E-mail Address" - password: "Password" - forgot_your_pass: "Forgot your password?" - need_an_account: "Need an account?" - register: "Register" - sign_up: "Sign Up" - confirm_password: "Confirm Password" - username: "Username" - already_have_an_account: "Already have an account?" - recover_password: "Recover password" - email_in_use: "Email address already in use" - email_or_username_in_use: "Email address or Username already in use" - required_field: "This field is required" - passwords_dont_match: "Passwords don't match." - special_characters: "Usernames can contain letters, numbers and _ only" - sign_in_to_comment: "Sign in to comment" - check_the_form: "Invalid Form. Please, check the fields" - createdisplay: - check_the_form: "Invalid Form. Please check the fields" - continue: "Continue with the same Facebook username" - error_create: "Error when changing username" - fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section." - fake_comment_date: "1 minute ago" - if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments." - required_field: "Required field" - save: Save - special_characters: "Usernames can contain letters numbers and _ only" - username: Username - write_your_username: "Edit your username" - your_username: "Your username appears on every comment you post." + talk-plugin-auth: + login: + email_verify_cta: "Please verify your email address." + request_new_verify_email: "Request another email" + verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." + verify_email2: "You must verify your account before engaging with the community." + not_you: "Not you?" + logged_in_as: "Signed in as" + facebook_sign_in: "Sign in with Facebook" + facebook_sign_up: "Sign up with Facebook" + logout: "Sign out" + sign_in: "Sign in" + sign_in_to_join: "Sign in to join the conversation" + or: "Or" + email: "E-mail Address" + password: "Password" + forgot_your_pass: "Forgot your password?" + need_an_account: "Need an account?" + register: "Register" + sign_up: "Sign Up" + confirm_password: "Confirm Password" + username: "Username" + already_have_an_account: "Already have an account?" + recover_password: "Recover password" + email_in_use: "Email address already in use" + email_or_username_in_use: "Email address or Username already in use" + required_field: "This field is required" + passwords_dont_match: "Passwords don't match." + special_characters: "Usernames can contain letters, numbers and _ only" + sign_in_to_comment: "Sign in to comment" + check_the_form: "Invalid Form. Please, check the fields" + set_username_dialog: + check_the_form: "Invalid Form. Please check the fields" + continue: "Continue with the same Facebook username" + error_create: "Error when changing username" + fake_comment_body: "This is an example comment. Readers can share their thoughts and opinions with newsrooms in the comments section." + fake_comment_date: "1 minute ago" + if_you_dont_change_your_name: "If you don't change your username at this step your Facebook display name will appear alongside of all your comments." + required_field: "Required field" + save: Save + special_characters: "Usernames can contain letters numbers and _ only" + username: Username + write_your_username: "Edit your username" + your_username: "Your username appears on every comment you post." zh_CN: - sign_in: - email_verify_cta: "请确认您的邮箱地址。" - request_new_verify_email: "请求再次发送邮件" - verify_email: "感谢您创建帐号。我们已向您提供的地址发送了封邮件,您可以通过邮件验证帐号。" - verify_email2: "您参与社群前须验证帐号。" - not_you: "不是你?" - logged_in_as: "登录身份" - facebook_sign_in: "使用 Facebook 帐号" - facebook_sign_up: "使用 Facebook 帐号" - logout: "登出" - sign_in: "登入" - sign_in_to_join: "登入以加入对话" - or: "或" - email: "邮箱地址" - password: "密码" - forgot_your_pass: "忘记密码?" - need_an_account: "需要帐号?" - register: "注册" - sign_up: "注册" - confirm_password: "确认密码" - username: "用户名" - already_have_an_account: "已有帐号?" - recover_password: "恢复密码" - email_in_use: "邮箱地址已被使用" - email_or_username_in_use: "邮箱地址或用户名已被使用" - required_field: "该字段必填" - passwords_dont_match: "密码不匹配。" - special_characters: "用户名只能包含字母、数字跟下划线" - sign_in_to_comment: "登入以评论" - check_the_form: "表格无效。请检查字段。" - createdisplay: - check_the_form: "表格无效。请检查字段。" - continue: "使用 Facebook 用户名" - error_create: "更改用户名时发生了错误" - fake_comment_body: "此为评论样例。读者可以在评论部分中分享他们关于新闻编辑室的想法和意见。" - fake_comment_date: "1 分钟前" - if_you_dont_change_your_name: "若您不在此改变用户名,您的 Facebook 用户名将随您的评论一同出现。" - required_field: "必填字段" - save: "保存" - special_characters: "用户名只能包含字母、数字跟下划线" - username: "用户名" - write_your_username: "编辑您的用户名" - your_username: "您的用户名将随您发表的所有评论一同出现。" + talk-plugin-auth: + login: + email_verify_cta: "请确认您的邮箱地址。" + request_new_verify_email: "请求再次发送邮件" + verify_email: "感谢您创建帐号。我们已向您提供的地址发送了封邮件,您可以通过邮件验证帐号。" + verify_email2: "您参与社群前须验证帐号。" + not_you: "不是你?" + logged_in_as: "登录身份" + logout: "登出" + sign_in: "登入" + sign_in_to_join: "登入以加入对话" + or: "或" + email: "邮箱地址" + password: "密码" + forgot_your_pass: "忘记密码?" + need_an_account: "需要帐号?" + register: "注册" + sign_up: "注册" + confirm_password: "确认密码" + username: "用户名" + already_have_an_account: "已有帐号?" + recover_password: "恢复密码" + email_in_use: "邮箱地址已被使用" + email_or_username_in_use: "邮箱地址或用户名已被使用" + required_field: "该字段必填" + passwords_dont_match: "密码不匹配。" + special_characters: "用户名只能包含字母、数字跟下划线" + sign_in_to_comment: "登入以评论" + check_the_form: "表格无效。请检查字段。" + set_username_dialog: + check_the_form: "表格无效。请检查字段。" + continue: "使用 Facebook 用户名" + error_create: "更改用户名时发生了错误" + fake_comment_body: "此为评论样例。读者可以在评论部分中分享他们关于新闻编辑室的想法和意见。" + fake_comment_date: "1 分钟前" + if_you_dont_change_your_name: "若您不在此改变用户名,您的 Facebook 用户名将随您的评论一同出现。" + required_field: "必填字段" + save: "保存" + special_characters: "用户名只能包含字母、数字跟下划线" + username: "用户名" + write_your_username: "编辑您的用户名" + your_username: "您的用户名将随您发表的所有评论一同出现。" zh_TW: - sign_in: - email_verify_cta: "請確認您的郵箱地址。" - request_new_verify_email: "請求再次發送郵件" - verify_email: "感謝您創建帳號。我們已向您提供的地址發送了封郵件,您可以通過郵件驗證帳號。" - verify_email2: "您參與社群前須驗證帳號。" - not_you: "不是你?" - logged_in_as: "登錄身份" - facebook_sign_in: "使用 Facebook 帳號" - facebook_sign_up: "使用 Facebook 帳號" - logout: "登出" - sign_in: "登入" - sign_in_to_join: "登入以加入對話" - or: "或" - email: "郵箱地址" - password: "密碼" - forgot_your_pass: "忘記密碼?" - need_an_account: "需要帳號?" - register: "註冊" - sign_up: "註冊" - confirm_password: "確認密碼" - username: "用戶名" - already_have_an_account: "已有帳號?" - recover_password: "恢覆密碼" - email_in_use: "郵箱地址已被使用" - email_or_username_in_use: "郵箱地址或用戶名已被使用" - required_field: "該字段必填" - passwords_dont_match: "密碼不匹配。" - special_characters: "用戶名只能包含字母、數字跟下劃線" - sign_in_to_comment: "登入以評論" - check_the_form: "表格無效。請檢查字段。" - createdisplay: - check_the_form: "表格無效。請檢查字段。" - continue: "使用 Facebook 用戶名" - error_create: "更改用戶名時發生了錯誤" - fake_comment_body: "此為評論樣例。讀者可以在評論部分中分享他們關於新聞編輯室的想法和意見。" - fake_comment_date: "1 分鐘前" - if_you_dont_change_your_name: "若您不在此改變用戶名,您的 Facebook 用戶名將隨您的評論一同出現。" - required_field: "必填字段" - save: "保存" - special_characters: "用戶名只能包含字母、數字跟下劃線" - username: "用戶名" - write_your_username: "編輯您的用戶名" - your_username: "您的用戶名將隨您發表的所有評論一同出現。" + talk-plugin-auth: + login: + email_verify_cta: "請確認您的郵箱地址。" + request_new_verify_email: "請求再次發送郵件" + verify_email: "感謝您創建帳號。我們已向您提供的地址發送了封郵件,您可以通過郵件驗證帳號。" + verify_email2: "您參與社群前須驗證帳號。" + not_you: "不是你?" + logged_in_as: "登錄身份" + logout: "登出" + sign_in: "登入" + sign_in_to_join: "登入以加入對話" + or: "或" + email: "郵箱地址" + password: "密碼" + forgot_your_pass: "忘記密碼?" + need_an_account: "需要帳號?" + register: "註冊" + sign_up: "註冊" + confirm_password: "確認密碼" + username: "用戶名" + already_have_an_account: "已有帳號?" + recover_password: "恢覆密碼" + email_in_use: "郵箱地址已被使用" + email_or_username_in_use: "郵箱地址或用戶名已被使用" + required_field: "該字段必填" + passwords_dont_match: "密碼不匹配。" + special_characters: "用戶名只能包含字母、數字跟下劃線" + sign_in_to_comment: "登入以評論" + check_the_form: "表格無效。請檢查字段。" + set_username_dialog: + check_the_form: "表格無效。請檢查字段。" + continue: "使用 Facebook 用戶名" + error_create: "更改用戶名時發生了錯誤" + fake_comment_body: "此為評論樣例。讀者可以在評論部分中分享他們關於新聞編輯室的想法和意見。" + fake_comment_date: "1 分鐘前" + if_you_dont_change_your_name: "若您不在此改變用戶名,您的 Facebook 用戶名將隨您的評論一同出現。" + required_field: "必填字段" + save: "保存" + special_characters: "用戶名只能包含字母、數字跟下劃線" + username: "用戶名" + write_your_username: "編輯您的用戶名" + your_username: "您的用戶名將隨您發表的所有評論一同出現。" diff --git a/plugins/talk-plugin-facebook-auth/client/.eslintrc.json b/plugins/talk-plugin-facebook-auth/client/.eslintrc.json new file mode 100644 index 000000000..c8a6db18a --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "@coralproject/eslint-config-talk/client" +} diff --git a/plugins/talk-plugin-facebook-auth/client/actions.js b/plugins/talk-plugin-facebook-auth/client/actions.js new file mode 100644 index 000000000..5a9ebc66f --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/actions.js @@ -0,0 +1,7 @@ +export const loginWithFacebook = () => (dispatch, _, { rest }) => { + window.open( + `${rest.uri}/auth/facebook`, + 'Continue with Facebook', + 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' + ); +}; diff --git a/plugins/talk-plugin-facebook-auth/client/components/FacebookButton.css b/plugins/talk-plugin-facebook-auth/client/components/FacebookButton.css new file mode 100644 index 000000000..694d6a322 --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/FacebookButton.css @@ -0,0 +1,13 @@ +.button { + background-color: #4267b2; + border-color: #4267b2; + color: rgb(255, 255, 255); + width: 100%; + box-sizing: border-box; + padding: 10px 20px; +} + +.button:hover { + background-color: #365899; + border-color: #365899; +} diff --git a/plugins/talk-plugin-facebook-auth/client/components/FacebookButton.js b/plugins/talk-plugin-facebook-auth/client/components/FacebookButton.js new file mode 100644 index 000000000..d9d1c0ab1 --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/FacebookButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { BareButton } from 'plugin-api/beta/client/components/ui'; +import styles from './FacebookButton.css'; + +export default ({ onClick, children }) => { + return ( + + {children} + + ); +}; diff --git a/plugins/talk-plugin-facebook-auth/client/components/SignIn.js b/plugins/talk-plugin-facebook-auth/client/components/SignIn.js new file mode 100644 index 000000000..1909c1aba --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/SignIn.js @@ -0,0 +1,9 @@ +import React from 'react'; +import FacebookButton from '../containers/FacebookButton'; +import { t } from 'plugin-api/beta/client/services'; + +export default () => { + return ( + {t('talk-plugin-facebook-auth.sign_in')} + ); +}; diff --git a/plugins/talk-plugin-facebook-auth/client/components/SignUp.js b/plugins/talk-plugin-facebook-auth/client/components/SignUp.js new file mode 100644 index 000000000..b0a185e3f --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/components/SignUp.js @@ -0,0 +1,9 @@ +import React from 'react'; +import FacebookButton from '../containers/FacebookButton'; +import { t } from 'plugin-api/beta/client/services'; + +export default () => { + return ( + {t('talk-plugin-facebook-auth.sign_up')} + ); +}; diff --git a/plugins/talk-plugin-facebook-auth/client/containers/FacebookButton.js b/plugins/talk-plugin-facebook-auth/client/containers/FacebookButton.js new file mode 100644 index 000000000..8ae3ff01e --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/containers/FacebookButton.js @@ -0,0 +1,9 @@ +import { connect } from 'plugin-api/beta/client/hocs'; +import { bindActionCreators } from 'redux'; +import { loginWithFacebook } from '../actions'; +import FacebookButton from '../components/FacebookButton'; + +const mapDispatchToProps = dispatch => + bindActionCreators({ onClick: loginWithFacebook }, dispatch); + +export default connect(null, mapDispatchToProps)(FacebookButton); diff --git a/plugins/talk-plugin-facebook-auth/client/index.js b/plugins/talk-plugin-facebook-auth/client/index.js new file mode 100644 index 000000000..cb8a8f059 --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/index.js @@ -0,0 +1,11 @@ +import SignIn from './components/SignIn'; +import SignUp from './components/SignUp'; +import translations from './translations.yml'; + +export default { + translations, + slots: { + authExternalSignIn: [SignIn], + authExternalSignUp: [SignUp], + }, +}; diff --git a/plugins/talk-plugin-facebook-auth/client/translations.yml b/plugins/talk-plugin-facebook-auth/client/translations.yml new file mode 100644 index 000000000..35a5c255c --- /dev/null +++ b/plugins/talk-plugin-facebook-auth/client/translations.yml @@ -0,0 +1,20 @@ +en: + talk-plugin-facebook-auth: + sign_in: "Sign in with Facebook" + sign_up: "Sign up with Facebook" +es: + talk-plugin-facebook-auth: + facebook_sign_in: "Entrar con Facebook" + facebook_sign_up: "Registrarse con Facebook" +fr: + talk-plugin-facebook-auth: + facebook_sign_in: "Connectez-vous avec Facebook" + facebook_sign_up: "Inscrivez-vous avec Facebook" +zh_CN: + talk-plugin-facebook-auth: + facebook_sign_in: "使用 Facebook 帐号" + facebook_sign_up: "使用 Facebook 帐号" +zh_TW: + talk-plugin-facebook-auth: + facebook_sign_in: "使用 Facebook 帳號" + facebook_sign_up: "使用 Facebook 帳號" diff --git a/routes/index.js b/routes/index.js index 5b1f90525..3727f76bb 100644 --- a/routes/index.js +++ b/routes/index.js @@ -74,6 +74,7 @@ router.use(compression()); //============================================================================== router.use('/admin', staticTemplate, require('./admin')); +router.use('/login', staticTemplate, require('./login')); router.use('/embed', staticTemplate, require('./embed')); //============================================================================== diff --git a/routes/login/index.js b/routes/login/index.js new file mode 100644 index 000000000..b69cb7f5c --- /dev/null +++ b/routes/login/index.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); + +router.get('*', (req, res) => { + res.render('login'); +}); + +module.exports = router; diff --git a/views/admin.ejs b/views/admin.ejs index da689f07f..939da0dcf 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -1,25 +1,9 @@ - Talk - Coral Admin - - - - - - - - - - - - - - - - <%_ if (locals.customCssUrl) { _%> - - <%_ } _%> - <% if (data != null) { %> - - <% } %> - + <%- include partials/head %>
diff --git a/views/admin/confirm-email.ejs b/views/admin/confirm-email.ejs index 8c0d85e01..e63910734 100644 --- a/views/admin/confirm-email.ejs +++ b/views/admin/confirm-email.ejs @@ -1,15 +1,11 @@ - Email Verification - - <%_ if (locals.customCssUrl) { _%> - - <%_ } _%> + <%- include partials/head %>
diff --git a/views/admin/docs.ejs b/views/admin/docs.ejs index 7c6ad9251..bfc476ee3 100644 --- a/views/admin/docs.ejs +++ b/views/admin/docs.ejs @@ -1,7 +1,6 @@ - Talk: GraphQL Docs - <%_ if (locals.customCssUrl) { _%> - - <%_ } _%> + <%- include partials/head %>
diff --git a/views/admin/password-reset.ejs b/views/admin/password-reset.ejs index ae06b5179..f66db491a 100644 --- a/views/admin/password-reset.ejs +++ b/views/admin/password-reset.ejs @@ -1,15 +1,11 @@ - Password Reset - - <%_ if (locals.customCssUrl) { _%> - - <%_ } _%> + <%- include partials/head %>
diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index dcd84d5f7..f9af61f7c 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -1,18 +1,9 @@ - - - - <%_ if (locals.customCssUrl) { _%> - - <%_ } _%> - <%_ if (data != null) { _%> - - <%_ } _%> - + <%- include ../partials/head %>
diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 000000000..87c5929f6 --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,12 @@ + + + + + <%- include partials/head %> + + +
+ + + + diff --git a/views/partials/head.ejs b/views/partials/head.ejs new file mode 100644 index 000000000..bc8b949d3 --- /dev/null +++ b/views/partials/head.ejs @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + +<%_ if (locals.customCssUrl) { _%> + +<%_ } _%> +<%_ if (data != null) { _%> + +<%_ } _%> + \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 6a018caba..fb6240d07 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,7 @@ debug(`Using ${pluginsPath} as the plugin configuration path`); const buildTargets = [ 'coral-admin', + 'coral-login', 'coral-docs', { name: 'coral-auth-callback', disablePolyfill: true }, ]; diff --git a/yarn.lock b/yarn.lock index 473bce147..427876288 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7697,8 +7697,8 @@ react-portal@^4.1.2: prop-types "^15.5.8" react-recaptcha@^2.2.6: - version "2.3.5" - resolved "https://registry.yarnpkg.com/react-recaptcha/-/react-recaptcha-2.3.5.tgz#a5db337125bb00fb13c2fa2e4ebfbe8b0cd06bb7" + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-recaptcha/-/react-recaptcha-2.3.6.tgz#afe07b5552f3ea4d37ecd22d9881c2776719ec2b" react-redux@^4.4.5: version "4.4.8"