diff --git a/Dockerfile b/Dockerfile index dc7e64e31..7cd06e673 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:7.6 -# Install yarn -RUN npm install -g yarn +# Add node-gyp for bcrypt build support +RUN yarn global add node-gyp # Create app directory RUN mkdir -p /usr/src/app diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 6762bd70a..389e8b4d2 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,5 +1,29 @@ import * as actions from '../constants/auth'; -import coralApi from '../../../coral-framework/helpers/response'; +import coralApi from 'coral-framework/helpers/response'; + +// Log In. +export const handleLogin = (email, password) => dispatch => { + dispatch({type: actions.LOGIN_REQUEST}); + return coralApi('/auth/local', {method: 'POST', body: {email, password}}) + .then(result => { + const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length; + dispatch(checkLoginSuccess(result.user, isAdmin)); + }) + .catch(error => { + dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key}); + }); +}; + +const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST}); +const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS}); +const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE}); + +export const requestPasswordReset = email => dispatch => { + dispatch(forgotPassowordRequest(email)); + return coralApi('/account/password/reset', {method: 'POST', body: {email}}) + .then(() => dispatch(forgotPassowordSuccess())) + .catch(error => dispatch(forgotPassowordFailure(error))); +}; // Check Login diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js new file mode 100644 index 000000000..7c7d4b00f --- /dev/null +++ b/client/coral-admin/src/components/AdminLogin.js @@ -0,0 +1,92 @@ +import React, {PropTypes} from 'react'; +import Layout from 'coral-admin/src/components/ui/Layout'; +import styles from './NotFound.css'; +import {Button, TextField, Alert, Success} from 'coral-ui'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +class AdminLogin extends React.Component { + + constructor (props) { + super(props); + this.state = {email: '', password: '', requestPassword: false}; + } + + handleSignIn = e => { + e.preventDefault(); + this.props.handleLogin(this.state.email, this.state.password); + } + + handleRequestPassword = e => { + e.preventDefault(); + this.props.requestPasswordReset(this.state.email); + } + + render () { + const {errorMessage} = this.props; + const signInForm = ( +
+ {errorMessage && {lang.t(`errors.${errorMessage}`)}} + this.setState({email: e.target.value})} /> + this.setState({password: e.target.value})} + type='password' /> +
+ +

+ Forgot your password? { + e.preventDefault(); + this.setState({requestPassword: true}); + }}>Request a new one. +

+ + ); + const requestPasswordForm = ( + this.props.passwordRequestSuccess + ?

{ + location.href = location.href; + }}> + {this.props.passwordRequestSuccess} Sign in + +

+ :
+ this.setState({email: e.target.value})} /> + + + ); + return ( + +
+

Team sign in

+

Sign in to interact with your community.

+ { this.state.requestPassword ? requestPasswordForm : signInForm } +
+
+ ); + } +} + +AdminLogin.propTypes = { + handleLogin: PropTypes.func.isRequired, + passwordRequestSuccess: PropTypes.string, + loginError: PropTypes.string +}; + +export default AdminLogin; diff --git a/client/coral-admin/src/components/NotFound.css b/client/coral-admin/src/components/NotFound.css index fc9aedc6b..9977f858a 100644 --- a/client/coral-admin/src/components/NotFound.css +++ b/client/coral-admin/src/components/NotFound.css @@ -1,12 +1,37 @@ .layout { - max-width: 800px; - margin: 0 auto; + max-width: 800px; + 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; + font-size: 40px; } -.layout img { - width: 100%; +.loginHeader { + font-size: 30px; +} + +.passwordRequestSuccess { + cursor: pointer; + padding: 8px 14px; } diff --git a/client/coral-admin/src/components/PermissionRequired.js b/client/coral-admin/src/components/PermissionRequired.js deleted file mode 100644 index c61ab1cef..000000000 --- a/client/coral-admin/src/components/PermissionRequired.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import {Layout} from 'react-mdl'; -import styles from './NotFound.css'; - -export const PermissionRequired = () => ( - -
-

Permission Required

-

We’re sorry, but you don’t have access to that page.

- Communicorn -
-
-); diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/ui/Drawer.js index 8fdf4f19d..9af651f3b 100644 --- a/client/coral-admin/src/components/ui/Drawer.js +++ b/client/coral-admin/src/components/ui/Drawer.js @@ -1,11 +1,11 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Navigation, Drawer} from 'react-mdl'; import {IndexLink, Link} from 'react-router'; import styles from './Drawer.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; -export default ({handleLogout, restricted = false}) => ( +const CoralDrawer = ({handleLogout, restricted = false}) => ( { !restricted ?
@@ -45,5 +45,11 @@ export default ({handleLogout, restricted = false}) => ( ); +CoralDrawer.propTypes = { + handleLogout: PropTypes.func.isRequired, + restricted: PropTypes.bool // hide app elements from a logged out user +}; + const lang = new I18n(translations); +export default CoralDrawer; diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 556b33b6c..4a2314926 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl'; import {Link, IndexLink} from 'react-router'; import styles from './Header.css'; @@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../../translations.json'; import {Logo} from './Logo'; -export default ({handleLogout, restricted = false}) => ( +const CoralHeader = ({handleLogout, restricted = false}) => (
{ @@ -64,4 +64,11 @@ export default ({handleLogout, restricted = false}) => (
); +CoralHeader.propTypes = { + handleLogout: PropTypes.func.isRequired, + restricted: PropTypes.bool // hide elemnts from a user that's logged out +}; + const lang = new I18n(translations); + +export default CoralHeader; diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js index f28e8dd80..c8b5bf57a 100644 --- a/client/coral-admin/src/components/ui/Layout.js +++ b/client/coral-admin/src/components/ui/Layout.js @@ -1,15 +1,22 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import {Layout as LayoutMDL} from 'react-mdl'; import Header from './Header'; import Drawer from './Drawer'; import styles from './Layout.css'; -export const Layout = ({children, ...props}) => ( +const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => ( -
- -
+
+ +
{children}
); + +Layout.propTypes = { + handleLogout: PropTypes.func, + restricted: PropTypes.bool // hide elements from a user that's logged out +}; + +export default Layout; diff --git a/client/coral-admin/src/constants/auth.js b/client/coral-admin/src/constants/auth.js index f77deb670..3c47bfc45 100644 --- a/client/coral-admin/src/constants/auth.js +++ b/client/coral-admin/src/constants/auth.js @@ -7,3 +7,11 @@ export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; + +export const LOGIN_REQUEST = 'LOGIN_REQUEST'; +export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; +export const LOGIN_FAILURE = 'LOGIN_FAILURE'; + +export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST'; +export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS'; +export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE'; diff --git a/client/coral-admin/src/containers/Configure/Configure.css b/client/coral-admin/src/containers/Configure/Configure.css index 1f23af30b..058ff7a64 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -3,7 +3,7 @@ h3 { color: black; - font-size: 1.76em; + font-size: 1.26em; font-weight: 500; } } @@ -24,6 +24,10 @@ margin-bottom: 20px; align-items: flex-start; min-height: 100px; + + h3 { + margin: 0; + } } .settingsError { @@ -118,18 +122,19 @@ } #bannedWordlist, #suspectWordlist { - width: 100%; - padding: 10px; - + display: block; input { width: 100%; } } -.wordlistHeader { - font-weight: bold; - font-size:18px; - margin-bottom:3px; +.customCSSInput { + width: 100%; + font-size: 14px; + padding: 14px; + letter-spacing: 0.03em; + color: #555; + box-sizing: border-box; } .enabledSetting { @@ -150,7 +155,7 @@ margin-top: 38px; } -.commentSettingsSection { +.settingsSection { padding-bottom: 200px; .action { display: inline-block; diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index 1ec78dcd2..b94394c61 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -8,22 +8,20 @@ import { updateDomainlist } from '../../actions/settings'; -import {Button, List, Item} from 'coral-ui'; +import {Button, List, Item, Card, Spinner} from 'coral-ui'; import styles from './Configure.css'; import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../../translations.json'; -import EmbedLink from './EmbedLink'; -import CommentSettings from './CommentSettings'; -import Wordlist from './Wordlist'; -import Domainlist from './Domainlist'; -import has from 'lodash/has'; +import translations from 'coral-admin/src/translations.json'; +import StreamSettings from './StreamSettings'; +import ModerationSettings from './ModerationSettings'; +import TechSettings from './TechSettings'; class Configure extends Component { constructor (props) { super(props); this.state = { - activeSection: 'comments', + activeSection: 'stream', changed: false, errors: {} }; @@ -70,40 +68,48 @@ class Configure extends Component { getSection (section) { const pageTitle = this.getPageTitle(section); + let sectionComponent; switch(section){ - case 'comments': - return ; - case 'embed': - return has(this, 'props.settings.domains.whitelist') - ?
- - -
- : ; - case 'wordlist': - return has(this, 'props.settings.wordlist') - ? - :

loading wordlists

; + break; + case 'moderation': + sectionComponent = ; + break; + case 'tech': + sectionComponent = ; } + + if (this.props.settings.fetchingSettings) { + return Loading settings...; + } + + return ( +
+

{pageTitle}

+ {sectionComponent} +
+ ); } getPageTitle (section) { switch(section) { - case 'comments': - return lang.t('configure.comment-settings'); - case 'embed': - return lang.t('configure.embed-comment-stream'); + case 'stream': + return lang.t('configure.stream-settings'); + case 'moderation': + return lang.t('configure.moderation-settings'); + case 'tech': + return lang.t('configure.tech-settings'); default: return ''; } @@ -120,14 +126,14 @@ class Configure extends Component {
- - {lang.t('configure.comment-settings')} + + {lang.t('configure.stream-settings')} - - {lang.t('configure.embed-comment-stream')} + + {lang.t('configure.moderation-settings')} - - {lang.t('configure.wordlist')} + + {lang.t('configure.tech-settings')}
diff --git a/client/coral-admin/src/containers/Configure/Domainlist.js b/client/coral-admin/src/containers/Configure/Domainlist.js index 6918c0471..c8c7cbd6c 100644 --- a/client/coral-admin/src/containers/Configure/Domainlist.js +++ b/client/coral-admin/src/containers/Configure/Domainlist.js @@ -9,19 +9,17 @@ const lang = new I18n(translations); const Domainlist = ({domains, onChangeDomainlist}) => { return ( -
+

{lang.t('configure.domain-list-title')}

- -

{lang.t('configure.domain-list-text')}

- data.split(',').map(d => d.trim())} - onChange={tags => onChangeDomainlist('whitelist', tags)} - /> -
-
+

{lang.t('configure.domain-list-text')}

+ data.split(',').map(d => d.trim())} + onChange={tags => onChangeDomainlist('whitelist', tags)} + /> + ); }; diff --git a/client/coral-admin/src/containers/Configure/EmbedLink.js b/client/coral-admin/src/containers/Configure/EmbedLink.js index 413f421d7..674bafa6e 100644 --- a/client/coral-admin/src/containers/Configure/EmbedLink.js +++ b/client/coral-admin/src/containers/Configure/EmbedLink.js @@ -44,19 +44,15 @@ class EmbedLink extends Component { "> `.trim(); return ( -
-

{this.props.title}

-
- -

{lang.t('configure.copy-and-paste')}

-