diff --git a/client/package.json b/client/package.json index 2cf86903..97e5359d 100644 --- a/client/package.json +++ b/client/package.json @@ -65,7 +65,7 @@ "react": "^15.0.1", "react-document-title": "^1.0.2", "react-dom": "^15.0.1", - "react-google-recaptcha": "^0.5.2", + "react-google-recaptcha": "^0.5.4", "react-motion": "^0.3.0", "react-redux": "^4.4.5", "react-router": "^2.4.0", diff --git a/client/src/actions/__tests__/config-actions-test.js b/client/src/actions/__tests__/config-actions-test.js index 0a0b7e05..164a7a1f 100644 --- a/client/src/actions/__tests__/config-actions-test.js +++ b/client/src/actions/__tests__/config-actions-test.js @@ -11,8 +11,8 @@ const ConfigActions = requireUnit('actions/config-actions', { describe('Config Actions,', function () { describe('init action', function () { - it('should return INIT_CONFIGS_FULFILLED with configs if it is already retrieved', function () { - sessionStoreMock.areConfigsStored.returns(true); + it('should return INIT_CONFIGS_FULFILLED with configs if it user is logged in', function () { + sessionStoreMock.isLoggedIn.returns(true); sessionStoreMock.getConfigs.returns({ config1: 'CONFIG_1', config2: 'CONFIG_2' @@ -31,7 +31,7 @@ describe('Config Actions,', function () { it('should return INIT_CONFIGS with API_RESULT if it is not retrieved', function () { APICallMock.call.reset(); - sessionStoreMock.areConfigsStored.returns(false); + sessionStoreMock.isLoggedIn.returns(false); sessionStoreMock.getConfigs.returns({ config1: 'CONFIG_1', config2: 'CONFIG_2' @@ -42,7 +42,7 @@ describe('Config Actions,', function () { payload: 'API_RESULT' }); expect(APICallMock.call).to.have.been.calledWith({ - path: '/system/get-configs', + path: '/system/get-settings', data: {} }); }); diff --git a/client/src/actions/config-actions.js b/client/src/actions/config-actions.js index 70c41c46..cbde584c 100644 --- a/client/src/actions/config-actions.js +++ b/client/src/actions/config-actions.js @@ -3,7 +3,7 @@ import sessionStore from 'lib-app/session-store'; export default { init() { - if (sessionStore.areConfigsStored()) { + if (sessionStore.isLoggedIn()) { return { type: 'INIT_CONFIGS_FULFILLED', payload: { @@ -14,7 +14,7 @@ export default { return { type: 'INIT_CONFIGS', payload: API.call({ - path: '/system/get-configs', + path: '/system/get-settings', data: {} }) }; diff --git a/client/src/actions/session-actions.js b/client/src/actions/session-actions.js index ed9490b4..3509d6a0 100644 --- a/client/src/actions/session-actions.js +++ b/client/src/actions/session-actions.js @@ -2,6 +2,8 @@ import API from 'lib-app/api-call'; import sessionStore from 'lib-app/session-store'; import store from 'app/store'; +import ConfigActions from 'actions/config-actions'; + export default { login(loginData) { return { diff --git a/client/src/app/main/captcha.js b/client/src/app/main/captcha.js new file mode 100644 index 00000000..41a4b3c3 --- /dev/null +++ b/client/src/app/main/captcha.js @@ -0,0 +1,35 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReCAPTCHA from 'react-google-recaptcha'; +import {connect} from 'react-redux'; + + +class Captcha extends React.Component { + constructor(props) { + super(props); + + this.state = { + value: '' + }; + } + + render() { + return ( + {this.setState({value})}} tabIndex="0" /> + ); + } + + getValue() { + return this.state.value; + } + + focus() { + ReactDOM.findDOMNode(this).focus(); + } +} + +export default connect((store) => { + return { + sitekey: store.config.reCaptchaKey + }; +}, null, null, { withRef: true })(Captcha); \ No newline at end of file diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js index f23e7fbe..ae981c6a 100644 --- a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js +++ b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js @@ -4,6 +4,7 @@ import { browserHistory } from 'react-router'; import i18n from 'lib-app/i18n'; import API from 'lib-app/api-call'; +import SessionStore from 'lib-app/session-store'; import store from 'app/store'; import SessionActions from 'actions/session-actions'; @@ -41,11 +42,7 @@ class CreateTicketForm extends React.Component {
{return {content: department}}), size: 'medium' }} />
@@ -70,7 +67,7 @@ class CreateTicketForm extends React.Component { renderCaptcha() { return (
- +
); } diff --git a/client/src/app/main/main-signup/main-signup-page.js b/client/src/app/main/main-signup/main-signup-page.js index 86d0fc00..b45afbba 100644 --- a/client/src/app/main/main-signup/main-signup-page.js +++ b/client/src/app/main/main-signup/main-signup-page.js @@ -1,9 +1,11 @@ import React from 'react'; -import ReCAPTCHA from 'react-google-recaptcha'; +import ReactDOM from 'react-dom'; +import _ from 'lodash'; import i18n from 'lib-app/i18n'; import API from 'lib-app/api-call'; +import Captcha from 'app/main/captcha'; import SubmitButton from 'core-components/submit-button'; import Message from 'core-components/message'; import Form from 'core-components/form'; @@ -34,7 +36,7 @@ class MainSignUpPageWidget extends React.Component {
- +
SIGN UP @@ -75,14 +77,20 @@ class MainSignUpPageWidget extends React.Component { } onLoginFormSubmit(formState) { - this.setState({ - loading: true - }); + const captcha = this.refs.captcha.getWrappedInstance(); - API.call({ - path: '/user/signup', - data: formState - }).then(this.onSignupSuccess.bind(this)).catch(this.onSignupFail.bind(this)); + if (!captcha.getValue()) { + captcha.focus(); + } else { + this.setState({ + loading: true + }); + + API.call({ + path: '/user/signup', + data: _.extend({captcha: captcha.getValue()}, formState) + }).then(this.onSignupSuccess.bind(this)).catch(this.onSignupFail.bind(this)); + } } onSignupSuccess() { diff --git a/client/src/data/fixtures/system-fixtures.js b/client/src/data/fixtures/system-fixtures.js index c74613f9..9dd58503 100644 --- a/client/src/data/fixtures/system-fixtures.js +++ b/client/src/data/fixtures/system-fixtures.js @@ -1,13 +1,18 @@ module.exports = [ { - path: '/system/get-configs', + path: '/system/get-settings', time: 1000, response: function () { return { status: 'success', data: { 'language': 'us', - 'reCaptchaKey': '6LfM5CYTAAAAAGLz6ctpf-hchX2_l0Ge-Bn-n8wS' + 'reCaptchaKey': '6LfM5CYTAAAAAGLz6ctpf-hchX2_l0Ge-Bn-n8wS', + 'departments': [ + 'Sales Support', + 'Technical Issues', + 'System and Administration' + ] } }; } diff --git a/client/src/lib-app/session-store.js b/client/src/lib-app/session-store.js index fa635f4e..94eac77f 100644 --- a/client/src/lib-app/session-store.js +++ b/client/src/lib-app/session-store.js @@ -42,6 +42,10 @@ class SessionStore { return JSON.parse(this.getItem('userData')); } + getDepartments() { + return JSON.parse(this.getItem('departments')); + } + storeRememberData({token, userId, expiration}) { this.setItem('rememberData-token', token); this.setItem('rememberData-userId', userId); @@ -51,19 +55,17 @@ class SessionStore { storeConfigs(configs) { this.setItem('language', configs.language); this.setItem('reCaptchaKey', configs.reCaptchaKey); + this.setItem('departments', JSON.stringify(configs.departments)); } getConfigs() { return { language: this.getItem('language'), - reCaptchaKey: this.getItem('reCaptchaKey') + reCaptchaKey: this.getItem('reCaptchaKey'), + departments: this.getDepartments() }; } - areConfigsStored() { - return !!this.getItem('reCaptchaKey'); - } - isRememberDataExpired() { let rememberData = this.getRememberData(); diff --git a/server/composer.json b/server/composer.json index fc910636..cd6c91e4 100644 --- a/server/composer.json +++ b/server/composer.json @@ -3,7 +3,8 @@ "slim/slim": "~2.0", "gabordemooij/redbean": "~4.2", "respect/validation": "^1.1", - "phpmailer/phpmailer": "^5.2" + "phpmailer/phpmailer": "^5.2", + "google/recaptcha": "~1.1" }, "require-dev": { "phpunit/phpunit": "5.0.*" diff --git a/server/controllers/system.php b/server/controllers/system.php index cea7f0c1..38e7db29 100644 --- a/server/controllers/system.php +++ b/server/controllers/system.php @@ -1,9 +1,11 @@ setGroupPath('/system'); $systemControllerGroup->addController(new InitSettingsController); +$systemControllerGroup->addController(new GetSettingsController); $systemControllerGroup->finalize(); \ No newline at end of file diff --git a/server/controllers/system/get-settings.php b/server/controllers/system/get-settings.php new file mode 100644 index 00000000..5593282c --- /dev/null +++ b/server/controllers/system/get-settings.php @@ -0,0 +1,20 @@ + 'any', + 'requestData' => [] + ]; + } + + public function handler() { + Response::respondSuccess([ + 'language' => Setting::getSetting('language')->getValue(), + 'reCaptchaKey' => Setting::getSetting('recaptcha-public')->getValue(), + 'departments' => Department::getDepartmentNames() + ]); + } +} \ No newline at end of file diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php index b17aca96..f03960c8 100644 --- a/server/controllers/system/init-settings.php +++ b/server/controllers/system/init-settings.php @@ -25,6 +25,8 @@ class InitSettingsController extends Controller { private function storeGlobalSettings() { $this->storeSettings([ 'language' => 'en', + 'recaptcha-public' => '', + 'recaptcha-private' => '', 'no-reply-email' => 'noreply@opensupports.com', 'smtp-host' => 'localhost', 'smtp-port' => 7070, diff --git a/server/controllers/user/signup.php b/server/controllers/user/signup.php index 7965baa6..4a188419 100644 --- a/server/controllers/user/signup.php +++ b/server/controllers/user/signup.php @@ -1,6 +1,7 @@ [ 'validation' => DataValidator::length(5, 200), 'error' => ERRORS::INVALID_PASSWORD + ], + 'captcha' => [ + 'validation' => DataValidator::captcha(), + 'error' => ERRORS::INVALID_CAPTCHA ] ] ]; diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index 1ade6895..82c6eba4 100644 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -13,4 +13,5 @@ class ERRORS { const INVALID_TICKET = 'Invalid ticket'; const INIT_SETTINGS_DONE = 'Settings already initialized'; const INVALID_OLD_PASSWORD = 'Invalid old password'; + const INVALID_CAPTCHA = 'Invalid captcha'; } diff --git a/server/index.php b/server/index.php index ae0b26e3..2adb7b1f 100644 --- a/server/index.php +++ b/server/index.php @@ -40,6 +40,7 @@ spl_autoload_register(function ($class) { //Load custom validations include_once 'libs/validations/dataStoreId.php'; include_once 'libs/validations/userEmail.php'; +include_once 'libs/validations/captcha.php'; // LOAD CONTROLLERS foreach (glob('controllers/*.php') as $controller) { diff --git a/server/libs/validations/captcha.php b/server/libs/validations/captcha.php new file mode 100644 index 00000000..437d92d8 --- /dev/null +++ b/server/libs/validations/captcha.php @@ -0,0 +1,19 @@ +getValue(); + + if (!$reCaptchaPrivateKey) return true; + + $reCaptcha = new \ReCaptcha\ReCaptcha($reCaptchaPrivateKey); + $reCaptchaValidation = $reCaptcha->verify($reCaptchaResponse, $_SERVER['REMOTE_ADDR']); + + return $reCaptchaValidation->isSuccess(); + } +} \ No newline at end of file diff --git a/server/models/Department.php b/server/models/Department.php index 05add3ff..a019bc77 100644 --- a/server/models/Department.php +++ b/server/models/Department.php @@ -1,4 +1,5 @@ name; + } + + return $departmentsNameList; + } } \ No newline at end of file diff --git a/server/run-tests.sh b/server/run-tests.sh index 487b82e1..251e7164 100755 --- a/server/run-tests.sh +++ b/server/run-tests.sh @@ -1,2 +1,3 @@ phpunit --colors tests/models -phpunit --colors tests/controllers \ No newline at end of file +phpunit --colors tests/controllers +phpunit --colors tests/libs \ No newline at end of file diff --git a/server/tests/__mocks__/BeanMock.php b/server/tests/__mocks__/BeanMock.php index 56d7568a..e47421e1 100644 --- a/server/tests/__mocks__/BeanMock.php +++ b/server/tests/__mocks__/BeanMock.php @@ -1,6 +1,7 @@ returns(new \Mock([ + 'isSuccess' => \Mock::stub()->returns($value) + ])); + } + + public function __construct($privateKey) { + parent::__construct(); + + $this->privateKey = $privateKey; + $this->verify = self::$staticVerify; + } + } +} \ No newline at end of file diff --git a/server/tests/__mocks__/RespectMock.php b/server/tests/__mocks__/RespectMock.php new file mode 100644 index 00000000..79d4d0eb --- /dev/null +++ b/server/tests/__mocks__/RespectMock.php @@ -0,0 +1,5 @@ +name = 'MOCK_SETTING_NAME'; $mockUserInstance->value = 'MOCK_SETTING_VALUE'; + $mockUserInstance->getValue = \Mock::stub()->returns('MOCK_SETTING_VALUE'); return $mockUserInstance; } diff --git a/server/tests/lib/.gitkeep b/server/tests/lib/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/server/tests/libs/validations/captchaTest.php b/server/tests/libs/validations/captchaTest.php new file mode 100644 index 00000000..e2e86eaf --- /dev/null +++ b/server/tests/libs/validations/captchaTest.php @@ -0,0 +1,35 @@ +validate('MOCK_RESPONSE'); + $this->assertTrue($response); + + \ReCaptcha\ReCaptcha::initVerify(false); + $response = $captchaValidation->validate('MOCK_RESPONSE'); + $this->assertFalse($response); + } + + public function testShouldPassCorrectValuesToCaptcha() { + $captchaValidation = new \CustomValidations\Captcha(); + $captchaValidation->validate('MOCK_RESPONSE'); + + $this->assertTrue(Setting::get('getSetting')->hasBeenCalledWithArgs('recaptcha-private')); + $this->assertTrue(\ReCaptcha\ReCaptcha::$staticVerify->hasBeenCalledWithArgs('MOCK_RESPONSE', 'MOCK_REMOTE')); + } +} \ No newline at end of file diff --git a/tests/init.rb b/tests/init.rb index eef9d99f..3948f566 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -10,6 +10,7 @@ require './scripts.rb' # TESTS require './system/init-settings.rb' +require './system/get-settings.rb' require './user/signup.rb' require './user/login.rb' require './user/send-recover-password.rb' diff --git a/tests/run-tests.sh b/tests/run-tests.sh index e87ac4ca..f8f7ff17 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -1,2 +1,4 @@ ./clean_db.sh +./clean_db.sh +./clean_db.sh bacon init.rb \ No newline at end of file diff --git a/tests/system/get-settings.rb b/tests/system/get-settings.rb new file mode 100644 index 00000000..437d6ee6 --- /dev/null +++ b/tests/system/get-settings.rb @@ -0,0 +1,11 @@ +describe '/system/get-settings' do + it 'should return correct values' do + result = request('/system/get-settings') + + (result['status']).should.equal('success') + (result['data']['language']).should.equal('en') + (result['data']['departments'][0]).should.equal('Tech Support') + (result['data']['departments'][1]).should.equal('Suggestions') + (result['data']['departments'][2]).should.equal('Sales and Subscriptions') + end +end \ No newline at end of file