diff --git a/client/src/app-components/__tests__/password-recovery-test.js b/client/src/app-components/__tests__/password-recovery-test.js new file mode 100644 index 00000000..906a2624 --- /dev/null +++ b/client/src/app-components/__tests__/password-recovery-test.js @@ -0,0 +1,91 @@ +const APICallMock = require('lib-app/__mocks__/api-call-mock'); + +const SubmitButton = ReactMock(); +const Button = ReactMock(); +const Input = ReactMock(); +const Form = ReactMock(); +const Checkbox = ReactMock(); +const Message = ReactMock(); +const FormField = ReactMock(); +const Widget = ReactMock(); + +const PasswordRecovery = requireUnit('app-components/password-recovery', { + 'lib-app/api-call': APICallMock, + 'core-components/submit-button': SubmitButton, + 'core-components/button': Button, + 'core-components/form-field': FormField, + 'core-components/': FormField, + 'core-components/form': Form, + 'core-components/checkbox': Checkbox, + 'core-components/message': Message, + 'core-components/widget': Widget, +}); + +describe('PasswordRecovery component', function () { + let recoverWidget, recoverForm, widgetTransition, emailInput, component, + backToLoginButton, submitButton; + + let dispatch = stub(); + + beforeEach(function () { + component = TestUtils.renderIntoDocument( + + ); + recoverWidget = TestUtils.scryRenderedComponentsWithType(component, Widget)[0]; + recoverForm = TestUtils.scryRenderedComponentsWithType(component, Form)[0]; + emailInput = TestUtils.scryRenderedComponentsWithType(component, Input)[0]; + submitButton = TestUtils.scryRenderedComponentsWithType(component, SubmitButton)[0]; + backToLoginButton = TestUtils.scryRenderedComponentsWithType(component, Button)[0]; + }); + + it('should control form errors by prop', function () { + expect(recoverForm.props.errors).to.deep.equal({}); + recoverForm.props.onValidateErrors({email: 'MOCK_ERROR'}); + expect(recoverForm.props.errors).to.deep.equal({email: 'MOCK_ERROR'}); + }); + + it('should call sendRecoverPassword when submitted', function () { + let mockSubmitData = {email: 'MOCK_VALUE'}; + APICallMock.call.reset(); + + recoverForm.props.onSubmit(mockSubmitData); + expect(APICallMock.call).to.have.been.calledWith({ + path: '/user/send-recover-password', + data: mockSubmitData + }); + }); + + it('should set loading true in the form when submitted', function () { + let mockSubmitData = {email: 'MOCK_VALUE'}; + + recoverForm.props.onSubmit(mockSubmitData); + expect(recoverForm.props.loading).to.equal(true); + }); + + it('should add error and stop loading when send recover fails', function () { + component.refs.recoverForm.refs.email.focus.reset(); + + component.onRecoverPasswordFail(); + expect(recoverForm.props.errors).to.deep.equal({email: 'EMAIL_NOT_EXIST'}); + expect(recoverForm.props.loading).to.equal(false); + expect(component.refs.recoverForm.refs.email.focus).to.have.been.called; + }); + + it('should show message when send recover success', function () { + let message = TestUtils.scryRenderedComponentsWithType(component, Message)[0]; + expect(message).to.equal(undefined); + + component.onRecoverPasswordSent(); + message = TestUtils.scryRenderedComponentsWithType(component, Message)[0]; + + expect(recoverForm.props.loading).to.equal(false); + expect(message).to.not.equal(null); + expect(message.props.type).to.equal('info'); + expect(message.props.children).to.equal('RECOVER_SENT'); + }); + + it('should show front side if \'Back to login form\' link is clicked', function () { + backToLoginButton.props.onClick(); + expect(widgetTransition.props.sideToShow).to.equal('front'); + }); +}); diff --git a/client/src/app-components/password-recovery.js b/client/src/app-components/password-recovery.js index 4541a5fc..aa667254 100644 --- a/client/src/app-components/password-recovery.js +++ b/client/src/app-components/password-recovery.js @@ -2,7 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import i18n from 'lib-app/i18n'; -import API from 'lib-app/api-call'; +import API from 'lib-app/api-call'; import Form from 'core-components/form'; import FormField from 'core-components/form-field'; @@ -12,7 +12,7 @@ import SubmitButton from 'core-components/submit-button'; import Message from 'core-components/message'; class PasswordRecovery extends React.Component { - + static propTypes = { recoverSent: React.PropTypes.bool, formProps: React.PropTypes.object, @@ -25,10 +25,11 @@ class PasswordRecovery extends React.Component { }; render() { + const { renderLogo, formProps, onBackToLoginClick } = this.props; return ( - + {this.renderLogo()} - + @@ -36,7 +37,7 @@ class PasswordRecovery extends React.Component { {i18n('RECOVER_PASSWORD')} - {event.preventDefault()}}> + {event.preventDefault()}}> {i18n('BACK_LOGIN_FORM')} {this.renderRecoverStatus()} @@ -44,6 +45,13 @@ class PasswordRecovery extends React.Component { ); } + getClass() { + return classNames({ + 'password-recovery__content': true, + [this.props.className]: (this.props.className) + }); + } + renderLogo() { let logo = null; diff --git a/client/src/app-components/ticket-viewer.js b/client/src/app-components/ticket-viewer.js index 601e25ad..ca67dc68 100644 --- a/client/src/app-components/ticket-viewer.js +++ b/client/src/app-components/ticket-viewer.js @@ -120,11 +120,11 @@ class TicketViewer extends React.Component { - {this.renderEditableOwnerNode()} + {this.renderAssignStaffList()} {ticket.closed ? - + {i18n('RE_OPEN')} : i18n('OPENED')} @@ -173,19 +173,6 @@ class TicketViewer extends React.Component { ); } - renderEditableOwnerNode() { - let ownerNode = null; - let {ticket, userId} = this.props; - - if (_.isEmpty(ticket.owner) || ticket.owner.id == userId) { - ownerNode = this.renderAssignStaffList(); - } else { - ownerNode = (this.props.ticket.owner) ? this.props.ticket.owner.name : i18n('NONE') - } - - return ownerNode; - } - renderOwnerNode() { let ownerNode = null; @@ -205,8 +192,6 @@ class TicketViewer extends React.Component { let selectedIndex = _.findIndex(items, {id: ownerId}); selectedIndex = (selectedIndex !== -1) ? selectedIndex : 0; - console.log(selectedIndex); - return ( { diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js b/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js index 817ff25f..54378546 100644 --- a/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js +++ b/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js @@ -1,6 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import {connect} from 'react-redux'; +import history from 'lib-app/history'; import SessionActions from 'actions/session-actions'; import CreateTicketForm from 'app/main/dashboard/dashboard-create-ticket/create-ticket-form'; diff --git a/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js b/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js index c5a9fd14..abe001d5 100644 --- a/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js +++ b/client/src/app/main/main-home/__tests__/main-home-page-login-widget-test.js @@ -1,4 +1,4 @@ -/*const SessionActionsMock = require('actions/__mocks__/session-actions-mock'); +const SessionActionsMock = require('actions/__mocks__/session-actions-mock'); const APICallMock = require('lib-app/__mocks__/api-call-mock'); const SubmitButton = ReactMock(); @@ -9,11 +9,13 @@ const Checkbox = ReactMock(); const Message = ReactMock(); const Widget = ReactMock(); const WidgetTransition = ReactMock(); +const PasswordRecovery = ReactMock(); const MainHomePageLoginWidget = requireUnit('app/main/main-home/main-home-page-login-widget', { 'react-redux': ReduxMock, 'actions/session-actions': SessionActionsMock, 'lib-app/api-call': APICallMock, + 'app-components/password-recovery': PasswordRecovery, 'core-components/submit-button': SubmitButton, 'core-components/button': Button, 'core-components/input': Input, @@ -98,7 +100,7 @@ describe('Login/Recover Widget', function () { expect(loginForm.props.errors).to.deep.equal({password: 'ERROR_PASSWORD'}); expect(loginForm.props.loading).to.equal(false); }); - + it('should show back side if \'Forgot your password?\' link is clicked', function () { expect(widgetTransition.props.sideToShow).to.equal('front'); forgotPasswordButton.props.onClick(); @@ -107,8 +109,7 @@ describe('Login/Recover Widget', function () { }); describe('Recover Password form', function () { - let recoverWidget, recoverForm, widgetTransition, emailInput, component, - backToLoginButton, submitButton; + let recoverPassword, widgetTransition, component; let dispatch = stub(); @@ -117,32 +118,20 @@ describe('Login/Recover Widget', function () { ); widgetTransition = TestUtils.scryRenderedComponentsWithType(component, WidgetTransition)[0]; - recoverWidget = TestUtils.scryRenderedComponentsWithType(component, Widget)[1]; - recoverForm = TestUtils.scryRenderedComponentsWithType(component, Form)[1]; - emailInput = TestUtils.scryRenderedComponentsWithType(component, Input)[2]; - submitButton = TestUtils.scryRenderedComponentsWithType(component, SubmitButton)[1]; - backToLoginButton = TestUtils.scryRenderedComponentsWithType(component, Button)[1]; - - component.refs.recoverForm = { - refs: { - email: { - focus: stub() - } - } - }; + recoverPassword = TestUtils.scryRenderedComponentsWithType(component, PasswordRecovery)[0]; }); it('should control form errors by prop', function () { - expect(recoverForm.props.errors).to.deep.equal({}); - recoverForm.props.onValidateErrors({email: 'MOCK_ERROR'}); - expect(recoverForm.props.errors).to.deep.equal({email: 'MOCK_ERROR'}); + expect(recoverPassword.props.formProps.errors).to.deep.equal({}); + recoverPassword.props.formProps.onValidateErrors({email: 'MOCK_ERROR'}); + expect(recoverPassword.props.formProps.errors).to.deep.equal({email: 'MOCK_ERROR'}); }); it('should call sendRecoverPassword when submitted', function () { let mockSubmitData = {email: 'MOCK_VALUE'}; APICallMock.call.reset(); - recoverForm.props.onSubmit(mockSubmitData); + recoverPassword.props.formProps.onSubmit(mockSubmitData); expect(APICallMock.call).to.have.been.calledWith({ path: '/user/send-recover-password', data: mockSubmitData @@ -152,12 +141,12 @@ describe('Login/Recover Widget', function () { it('should set loading true in the form when submitted', function () { let mockSubmitData = {email: 'MOCK_VALUE'}; - recoverForm.props.onSubmit(mockSubmitData); - expect(recoverForm.props.loading).to.equal(true); + recoverPassword.props.formProps.onSubmit(mockSubmitData); + expect(recoverForm.props.formProps.loading).to.equal(true); }); it('should add error and stop loading when send recover fails', function () { - component.refs.recoverForm.refs.email.focus.reset(); + component.refs.recoverPassword.refs.email.focus.reset(); component.onRecoverPasswordFail(); expect(recoverForm.props.errors).to.deep.equal({email: 'EMAIL_NOT_EXIST'}); @@ -183,4 +172,4 @@ describe('Login/Recover Widget', function () { expect(widgetTransition.props.sideToShow).to.equal('front'); }); }); -});*/ +}); diff --git a/client/src/app/main/main-home/main-home-page-login-widget.js b/client/src/app/main/main-home/main-home-page-login-widget.js index 2a56aba0..e2e77b18 100644 --- a/client/src/app/main/main-home/main-home-page-login-widget.js +++ b/client/src/app/main/main-home/main-home-page-login-widget.js @@ -9,7 +9,7 @@ import API from 'lib-app/api-call'; import focus from 'lib-core/focus'; import i18n from 'lib-app/i18n'; -import PasswordRecovery from 'app-components/password-recovery.js'; +import PasswordRecovery from 'app-components/password-recovery'; import SubmitButton from 'core-components/submit-button'; import Button from 'core-components/button'; import Form from 'core-components/form'; diff --git a/client/src/core-components/widget-transition.js b/client/src/core-components/widget-transition.js index 218f392c..26cf0880 100644 --- a/client/src/core-components/widget-transition.js +++ b/client/src/core-components/widget-transition.js @@ -79,13 +79,13 @@ class WidgetTransition extends React.Component { rotateY: (this.props.sideToShow === 'front') ? spring(0, [100, 20]) : spring(180, [100, 20]) }; } - + moveFocusToCurrentSide() { let currentWidget; let previousWidget; if (this.props.sideToShow === 'front') { - currentWidget = this.primaryWidget; + currentWidget = this.primaryWidget; previousWidget = this.secondaryWidget; } else { currentWidget = this.secondaryWidget; diff --git a/client/src/lib-test/preprocessor.js b/client/src/lib-test/preprocessor.js index e3a1e44d..1904b33c 100644 --- a/client/src/lib-test/preprocessor.js +++ b/client/src/lib-test/preprocessor.js @@ -23,18 +23,19 @@ global.requireUnit = function (path, mocks) { }; global.reRenderIntoDocument = (function () { let div; - + return function (jsx) { if (!div) { div = document.createElement('div') } - + return ReactDOM.render(jsx, div); } })(); global.ReduxMock = { connect: stub().returns(stub().returnsArg(0)) }; +global.globalIndexPath = ''; Array.prototype.swap = function (x,y) { var b = this[x]; diff --git a/server/Makefile b/server/Makefile index 214dc9ea..37d92839 100644 --- a/server/Makefile +++ b/server/Makefile @@ -28,7 +28,9 @@ stop: @docker stop opensupports-db && docker rm opensupports-db || true @docker stop opensupports-myadmin && docker rm opensupports-myadmin || true @docker stop opensupports-fakesmtp && docker rm opensupports-fakesmtp || true - @docker stop opensupports-srv + @docker stop opensupports-srv || true + @rm -rf .fakemail || true + @mkdir .fakemail db: @docker exec -it opensupports-db bash -c "mysql -u root" || echo "${red}Please execute 'make run' first${reset}" diff --git a/server/controllers/staff/edit.php b/server/controllers/staff/edit.php index e6acc072..bdb6b811 100755 --- a/server/controllers/staff/edit.php +++ b/server/controllers/staff/edit.php @@ -80,14 +80,31 @@ class EditStaffController extends Controller { } if(Controller::request('departments') && Controller::isStaffLogged(3)) { - $this->staffInstance->sharedDepartmentList = $this->getDepartmentList(); + $departmentList = $this->getDepartmentList(); + $ticketList = $this->staffInstance->sharedTicketList; + + $this->staffInstance->sharedDepartmentList = $departmentList; + + foreach($ticketList as $ticket) { + if(!$departmentList->includesId($ticket->department->id)) { + if($ticket->isOwner($this->staffInstance) ) { + $ticket->owner = null; + } + + if(!$ticket->isAuthor($this->staffInstance)) { + $this->staffInstance->sharedTicketList->remove($ticket); + } + + $ticket->store(); + } + } } if($fileUploader = $this->uploadFile(true)) { $this->staffInstance->profilePic = ($fileUploader instanceof FileUploader) ? $fileUploader->getFileName() : null; } - if(Controller::request('sendEmailOnNewTicket') !== null && Controller::request('sendEmailOnNewTicket') !== '' && $this->isModifyingCurrentStaff()) { + if(Controller::request('sendEmailOnNewTicket') !== null && Controller::request('sendEmailOnNewTicket') !== '' && $this->isModifyingCurrentStaff()) { $this->staffInstance->sendEmailOnNewTicket = intval(Controller::request('sendEmailOnNewTicket')); } diff --git a/server/controllers/staff/un-assign-ticket.php b/server/controllers/staff/un-assign-ticket.php index 5a67592c..0ebb7e06 100755 --- a/server/controllers/staff/un-assign-ticket.php +++ b/server/controllers/staff/un-assign-ticket.php @@ -46,7 +46,7 @@ class UnAssignStaffController extends Controller { $owner = $ticket->owner; if($ticket->isOwner($user) || $user->level > 2) { - if(!$ticket->isAuthor($user)) { + if(!$ticket->isAuthor($owner)) { $owner->sharedTicketList->remove($ticket); $owner->store(); } diff --git a/server/controllers/ticket/get.php b/server/controllers/ticket/get.php index ada1167b..3bb12482 100755 --- a/server/controllers/ticket/get.php +++ b/server/controllers/ticket/get.php @@ -32,7 +32,7 @@ class TicketGetController extends Controller { public function validations() { $session = Session::getInstance(); - + if (Controller::isUserSystemEnabled() || Controller::isStaffLogged()) { return [ 'permission' => 'user', @@ -78,6 +78,6 @@ class TicketGetController extends Controller { $user = Controller::getLoggedUser(); return (!Controller::isStaffLogged() && (Controller::isUserSystemEnabled() && $this->ticket->author->id !== $user->id)) || - (Controller::isStaffLogged() && (($this->ticket->owner && $this->ticket->owner->id !== $user->id) && !$user->sharedDepartmentList->includesId($this->ticket->department->id))); + (Controller::isStaffLogged() && (!$user->sharedTicketList->includesId($this->ticket->id) && !$user->sharedDepartmentList->includesId($this->ticket->department->id))); } } diff --git a/server/controllers/ticket/re-open.php b/server/controllers/ticket/re-open.php index dfe4ffbc..9c022629 100755 --- a/server/controllers/ticket/re-open.php +++ b/server/controllers/ticket/re-open.php @@ -63,8 +63,13 @@ class ReOpenController extends Controller { private function shouldDenyPermission() { $user = Controller::getLoggedUser(); - return (!Controller::isStaffLogged() && $this->ticket->author->id !== $user->id) || - (Controller::isStaffLogged() && $this->ticket->owner && $this->ticket->owner->id !== $user->id); + return !( + $this->ticket->isAuthor($user) || + ( + Controller::isStaffLogged() && + $user->sharedDepartmentList->includesId($this->ticket->department->id) + ) + ); } private function markAsUnread() { diff --git a/server/controllers/user/recover-password.php b/server/controllers/user/recover-password.php index 1050d82d..49628f85 100755 --- a/server/controllers/user/recover-password.php +++ b/server/controllers/user/recover-password.php @@ -41,7 +41,10 @@ class RecoverPasswordController extends Controller { 'permission' => 'any', 'requestData' => [ 'email' => [ - 'validation' => DataValidator::email()->userEmail(), + 'validation' => DataValidator::oneOf( + DataValidator::email()->userEmail(), + DataValidator::email()->staffEmail() + ), 'error' => ERRORS::INVALID_EMAIL ], 'password' => [ diff --git a/server/controllers/user/send-recover-password.php b/server/controllers/user/send-recover-password.php index 04619319..ed924a80 100755 --- a/server/controllers/user/send-recover-password.php +++ b/server/controllers/user/send-recover-password.php @@ -38,7 +38,10 @@ class SendRecoverPasswordController extends Controller { 'permission' => 'any', 'requestData' => [ 'email' => [ - 'validation' => DataValidator::email()->userEmail(), + 'validation' => DataValidator::oneOf( + DataValidator::email()->userEmail(), + DataValidator::email()->staffEmail() + ), 'error' => ERRORS::INVALID_EMAIL ] ] @@ -66,7 +69,7 @@ class SendRecoverPasswordController extends Controller { $recoverPassword->setProperties(array( 'email' => $email, 'token' => $this->token, - 'staff' => $this->staff + 'staff' => !!$this->staff )); $recoverPassword->store(); diff --git a/server/index.php b/server/index.php index 5d0fbd07..66048610 100644 --- a/server/index.php +++ b/server/index.php @@ -47,9 +47,10 @@ spl_autoload_register(function ($class) { } }); -//Load custom validations +// LOAD CUSTOM VALIDATIONS include_once 'libs/validations/dataStoreId.php'; include_once 'libs/validations/userEmail.php'; +include_once 'libs/validations/staffEmail.php'; include_once 'libs/validations/captcha.php'; include_once 'libs/validations/validLanguage.php'; include_once 'libs/validations/validTicketNumber.php'; diff --git a/server/libs/Controller.php b/server/libs/Controller.php index 3d198dbc..d84ca066 100755 --- a/server/libs/Controller.php +++ b/server/libs/Controller.php @@ -55,6 +55,10 @@ abstract class Controller { public static function request($key, $secure = false) { $result = call_user_func(self::$dataRequester, $key); + if($key === 'email' || $key === 'newEmail') { + return strtolower($result); + } + if($secure) { $config = HTMLPurifier_Config::createDefault(); $purifier = new HTMLPurifier($config); diff --git a/server/libs/validations/staffEmail.php b/server/libs/validations/staffEmail.php new file mode 100644 index 00000000..98f82b42 --- /dev/null +++ b/server/libs/validations/staffEmail.php @@ -0,0 +1,14 @@ +isNull(); + } +} diff --git a/tests/staff/edit.rb b/tests/staff/edit.rb index b1c46f08..5c398936 100644 --- a/tests/staff/edit.rb +++ b/tests/staff/edit.rb @@ -16,7 +16,7 @@ describe'/staff/edit' do row = $database.getRow('staff', 3, 'id') - (row['email']).should.equal('LittleLannister@opensupports.com') + (row['email']).should.equal('littlelannister@opensupports.com') (row['level']).should.equal('1') rows = $database.getRow('department_staff', 3, 'staff_id')