diff --git a/client/src/app-components/activity-row.js b/client/src/app-components/activity-row.js index cb5dec18..1b3da113 100644 --- a/client/src/app-components/activity-row.js +++ b/client/src/app-components/activity-row.js @@ -23,6 +23,7 @@ class ActivityRow extends React.Component { 'EDIT_SETTINGS', 'SIGNUP', + 'INVITE', 'ADD_TOPIC', 'ADD_ARTICLE', 'DELETE_TOPIC', @@ -65,9 +66,7 @@ class ActivityRow extends React.Component {
- - {this.props.author.name} - + {this.renderAuthorName()} {i18n('ACTIVITY_' + this.props.type)} {_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : this.props.to} @@ -76,6 +75,18 @@ class ActivityRow extends React.Component { ); } + renderAuthorName() { + let name = this.props.author.name; + + if (this.props.author.id) { + name = + {this.props.author.name} + ; + } + + return name; + } + renderTicketNumber() { let ticketNumber = (this.props.mode === 'staff') ? this.props.ticketNumber : this.props.to; @@ -106,6 +117,7 @@ class ActivityRow extends React.Component { 'EDIT_SETTINGS': 'wrench', 'SIGNUP': 'user-plus', + 'INVITE': 'user-plus', 'ADD_TOPIC': 'book', 'ADD_ARTICLE': 'book', 'DELETE_TOPIC': 'book', diff --git a/client/src/app-components/ticket-list.js b/client/src/app-components/ticket-list.js index d23ecf0f..6f85dc43 100644 --- a/client/src/app-components/ticket-list.js +++ b/client/src/app-components/ticket-list.js @@ -274,4 +274,4 @@ export default connect((store) => { return { tags: store.config['tags'] }; -})(TicketList); \ No newline at end of file +})(TicketList); diff --git a/client/src/app-components/ticket-query-list.js b/client/src/app-components/ticket-query-list.js new file mode 100644 index 00000000..f28dbc3b --- /dev/null +++ b/client/src/app-components/ticket-query-list.js @@ -0,0 +1,90 @@ +import React from 'react'; +import _ from 'lodash'; + +import API from 'lib-app/api-call'; +import i18n from 'lib-app/i18n'; +import {connect} from 'react-redux'; + +import TicketList from 'app-components/ticket-list'; +import Message from 'core-components/message'; + +class TicketQueryList extends React.Component { + + state = { + tickets: [], + page: 1, + pages: 0, + error: null, + loading: true + }; + + componentDidMount() { + this.getTickets(); + } + + componentDidUpdate(prevProps) { + if (this.props.customList.title !== prevProps.customList.title) { + this.getTickets(); + } + } + + render() { + return ( +
+ {(this.state.error) ? {i18n('ERROR_RETRIEVING_TICKETS')} : } +
+ ); + } + + getTickets() { + this.setState({ + loading:true + }) + API.call({ + path: '/ticket/search', + data: { + page : this.state.page, + ...this.props.customList.filters + } + }).then((result) => { + this.setState({ + tickets: result.data.tickets, + page: result.data.page, + pages: result.data.pages, + error: null, + loading: false + }) + }).catch((result) => this.setState({ + loading: false, + error: result.message + })); + + } + + onPageChange(event) { + this.setState({page: event.target.value}, () => this.getTickets()); + } + + getTicketListProps () { + const {page,pages,loading,tickets} = this.state; + return { + userId: this.props.userId, + ticketPath: '/admin/panel/tickets/view-ticket/', + tickets, + page, + pages, + loading, + type: 'secondary', + showDepartmentDropdown: false, + closedTicketsShown: false, + onPageChange:this.onPageChange.bind(this) + }; + } + +} + +export default connect((store) => { + return { + userId: store.session.userId + }; +})(TicketQueryList); diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js index 0419c0db..b9d43065 100644 --- a/client/src/app/Routes.js +++ b/client/src/app/Routes.js @@ -33,6 +33,7 @@ import AdminPanelMyAccount from 'app/admin/panel/dashboard/admin-panel-my-accoun import AdminPanelMyTickets from 'app/admin/panel/tickets/admin-panel-my-tickets'; import AdminPanelNewTickets from 'app/admin/panel/tickets/admin-panel-new-tickets'; import AdminPanelAllTickets from 'app/admin/panel/tickets/admin-panel-all-tickets'; +import AdminPanelSearchTickets from 'app/admin/panel/tickets/admin-panel-search-tickets'; import AdminPanelViewTicket from 'app/admin/panel/tickets/admin-panel-view-ticket'; import AdminPanelCustomResponses from 'app/admin/panel/tickets/admin-panel-custom-responses'; @@ -113,6 +114,7 @@ export default ( + diff --git a/client/src/app/admin/panel/admin-panel-menu.js b/client/src/app/admin/panel/admin-panel-menu.js index da34ccb7..9627947a 100644 --- a/client/src/app/admin/panel/admin-panel-menu.js +++ b/client/src/app/admin/panel/admin-panel-menu.js @@ -76,7 +76,7 @@ class AdminPanelMenu extends React.Component { getGroupItemIndex() { const group = this.getRoutes()[this.getGroupIndex()]; - const pathname = this.props.location.pathname; + const pathname = this.props.location.pathname + this.props.location.search; return _.findIndex(group.items, {path: pathname}); } @@ -90,19 +90,35 @@ class AdminPanelMenu extends React.Component { return (groupIndex === -1) ? 0 : groupIndex; } + getCustomlists() { + if(window.customTicketList){ + return window.customTicketList.map((item, index) => { + return { + name: item.title, + path: '/admin/panel/tickets/search-tickets?custom=' + index, + level: 1 + } + }) + } else { + return []; + } + } + getRoutes() { - return this.getItemsByFilteredByLevel([ + const customLists = this.getCustomlists(); + + return this.getItemsByFilteredByLevel(_.without([ { groupName: i18n('DASHBOARD'), path: '/admin/panel', icon: 'tachometer', level: 1, items: this.getItemsByFilteredByLevel([ - { + /*{ name: i18n('STATISTICS'), path: '/admin/panel/stats', level: 1 - }, + },*/ { name: i18n('LAST_ACTIVITY'), path: '/admin/panel/activity', @@ -135,10 +151,11 @@ class AdminPanelMenu extends React.Component { name: i18n('CUSTOM_RESPONSES'), path: '/admin/panel/tickets/custom-responses', level: 2 - } + }, + ...customLists ]) }, - { + this.props.config['user-system-enabled'] ? { groupName: i18n('USERS'), path: '/admin/panel/users', icon: 'user', @@ -160,7 +177,7 @@ class AdminPanelMenu extends React.Component { level: 1 } ]) - }, + } : null, { groupName: i18n('ARTICLES'), path: '/admin/panel/articles', @@ -175,7 +192,6 @@ class AdminPanelMenu extends React.Component { ]) }, { - groupName: i18n('STAFF'), path: '/admin/panel/staff', icon: 'users', @@ -222,7 +238,7 @@ class AdminPanelMenu extends React.Component { } ]) } - ]); + ], null)); } getItemsByFilteredByLevel(items) { @@ -232,6 +248,7 @@ class AdminPanelMenu extends React.Component { export default connect((store) => { return { - level: store.session.userLevel + level: store.session.userLevel, + config: store.config }; })(AdminPanelMenu); diff --git a/client/src/app/admin/panel/staff/add-staff-modal.js b/client/src/app/admin/panel/staff/add-staff-modal.js index 8a20ed00..d9c77a0d 100644 --- a/client/src/app/admin/panel/staff/add-staff-modal.js +++ b/client/src/app/admin/panel/staff/add-staff-modal.js @@ -67,7 +67,7 @@ class AddStaffModal extends React.Component { return SessionStore.getDepartments().map(department => { if(department.private*1){ return {department.name} - }else { + } else { return department.name; } }); diff --git a/client/src/app/admin/panel/staff/admin-panel-staff-members.js b/client/src/app/admin/panel/staff/admin-panel-staff-members.js index ac587050..75689f87 100644 --- a/client/src/app/admin/panel/staff/admin-panel-staff-members.js +++ b/client/src/app/admin/panel/staff/admin-panel-staff-members.js @@ -11,7 +11,7 @@ import SessionStore from 'lib-app/session-store'; import PeopleList from 'app-components/people-list'; import ModalContainer from 'app-components/modal-container'; -import AddStaffModal from 'app/admin/panel/staff/add-staff-modal'; +import InviteStaffModal from 'app/admin/panel/staff/invite-staff-modal'; import Header from 'core-components/header'; import DropDown from 'core-components/drop-down'; @@ -47,8 +47,8 @@ class AdminPanelStaffMembers extends React.Component {
-
{(this.props.loading) ? : this.setState({page: index+1})} />} @@ -56,8 +56,8 @@ class AdminPanelStaffMembers extends React.Component { ); } - onAddNewStaff() { - ModalContainer.openModal(); + onInviteStaff() { + ModalContainer.openModal(); } getDepartmentDropdownProps() { diff --git a/client/src/app/admin/panel/staff/invite-staff-modal.js b/client/src/app/admin/panel/staff/invite-staff-modal.js new file mode 100644 index 00000000..c98e608c --- /dev/null +++ b/client/src/app/admin/panel/staff/invite-staff-modal.js @@ -0,0 +1,120 @@ +import React from 'react'; +import _ from 'lodash'; + +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; +import SessionStore from 'lib-app/session-store'; + +import Header from 'core-components/header' +import Form from 'core-components/form'; +import FormField from 'core-components/form-field'; +import SubmitButton from 'core-components/submit-button'; +import Button from 'core-components/button'; +import Icon from 'core-components/icon'; + +class InviteStaffModal extends React.Component { + + static contextTypes = { + closeModal: React.PropTypes.func + }; + + static propTypes = { + onSuccess: React.PropTypes.func + }; + + state = { + loading: false, + errors: {}, + error: null + }; + + render() { + return ( +
+
+
this.setState({errors})} loading={this.state.loading}> +
+
+ + +
+ +
+
+
+
+
{i18n('Departments')}
+ +
+
+
+ + {i18n('SAVE')} + + +
+
+ ); + } + + getDepartments() { + return SessionStore.getDepartments().map(department => { + if(department.private*1){ + return {department.name} + } else { + return department.name; + } + }); + } + + onSubmit(form) { + let departments = _.filter(SessionStore.getDepartments(), (department, index) => { + return _.includes(form.departments, index); + }).map(department => department.id); + + this.setState({loading: true}); + + API.call({ + path: '/staff/invite', + data: { + name: form.name, + email: form.email, + level: form.level + 1, + departments: JSON.stringify(departments) + } + }).then(() => { + this.context.closeModal(); + + if(this.props.onSuccess) { + this.props.onSuccess(); + } + }).catch((result) => { + this.setState({ + loading: false, + error: result.message + }); + }); + } + + onCancelClick(event) { + event.preventDefault(); + this.context.closeModal(); + } + + getErrors() { + let errors = _.extend({}, this.state.errors); + + if (this.state.error === 'ALREADY_A_STAFF') { + errors.email = i18n('EMAIL_EXISTS'); + } + + return errors; + } +} + +export default InviteStaffModal; diff --git a/client/src/app/admin/panel/staff/invite-staff-modal.scss b/client/src/app/admin/panel/staff/invite-staff-modal.scss new file mode 100644 index 00000000..60c35e1c --- /dev/null +++ b/client/src/app/admin/panel/staff/invite-staff-modal.scss @@ -0,0 +1,23 @@ +@import "../../../../scss/vars"; + +.invite-staff-modal { + width: 700px; + + &__level-selector { + text-align: center; + } + + &__departments { + @include scrollbars(); + + border: 1px solid $grey; + padding: 20px; + height: 320px; + overflow-y: auto; + } + + &__departments-title { + font-size: $font-size--md; + text-align: center; + } +} \ No newline at end of file diff --git a/client/src/app/admin/panel/tickets/admin-panel-search-tickets.js b/client/src/app/admin/panel/tickets/admin-panel-search-tickets.js new file mode 100644 index 00000000..f5726f2c --- /dev/null +++ b/client/src/app/admin/panel/tickets/admin-panel-search-tickets.js @@ -0,0 +1,34 @@ +import React from 'react'; +import {connect} from 'react-redux'; + +import i18n from 'lib-app/i18n'; + +import TicketQueryList from 'app-components/ticket-query-list'; + +import Header from 'core-components/header'; +import Message from 'core-components/message'; + +class AdminPanelSearchTickets extends React.Component { + + render() { + return ( +
+
+ {(this.props.error) ? {i18n('ERROR_RETRIEVING_TICKETS')} : } +
+ ); + } + + getFilters() { + let customList = (window.customTicketList && window.customTicketList[this.props.location.query.custom*1]) ? window.customTicketList[this.props.location.query.custom*1] : null + return { + ...customList + }; + } +} + +export default connect((store) => { + return { + error: store.adminData.allTicketsError + }; +})(AdminPanelSearchTickets); diff --git a/client/src/app/admin/panel/users/admin-panel-list-users.js b/client/src/app/admin/panel/users/admin-panel-list-users.js index b2997af2..17872138 100644 --- a/client/src/app/admin/panel/users/admin-panel-list-users.js +++ b/client/src/app/admin/panel/users/admin-panel-list-users.js @@ -1,4 +1,5 @@ import React from 'react'; +import {connect} from 'react-redux'; import i18n from 'lib-app/i18n'; import API from 'lib-app/api-call'; @@ -12,10 +13,9 @@ import Button from 'core-components/button'; import Message from 'core-components/message'; import Icon from 'core-components/icon'; import ModalContainer from 'app-components/modal-container'; -import MainSignUpWidget from 'app/main/main-signup/main-signup-widget'; +import InviteUserWidget from 'app/admin/panel/users/invite-user-widget'; class AdminPanelListUsers extends React.Component { - state = { loading: true, users: [], @@ -39,11 +39,19 @@ class AdminPanelListUsers extends React.Component { return (
+ {(this.state.error) ? {i18n('ERROR_RETRIEVING_USERS')} : this.renderTableAndInviteButton()} +
+ ); + } + + renderTableAndInviteButton() { + return ( +
- {(this.state.error) ? {i18n('ERROR_RETRIEVING_USERS')} : } +
-
@@ -167,17 +175,17 @@ class AdminPanelListUsers extends React.Component { }).catch(this.onUsersRejected.bind(this)).then(this.onUsersRetrieved.bind(this)); } - onCreateUser(user) { + onInviteUser(user) { ModalContainer.openModal( -
- +
+
); } - onCreateUserSuccess() { + onInviteUserSuccess() { ModalContainer.closeModal(); } @@ -201,4 +209,8 @@ class AdminPanelListUsers extends React.Component { } } -export default AdminPanelListUsers; +export default connect((store) => { + return { + config: store.config + }; +})(AdminPanelListUsers); diff --git a/client/src/app/admin/panel/users/invite-user-widget.js b/client/src/app/admin/panel/users/invite-user-widget.js new file mode 100644 index 00000000..8336724d --- /dev/null +++ b/client/src/app/admin/panel/users/invite-user-widget.js @@ -0,0 +1,165 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import _ from 'lodash'; +import classNames from 'classnames'; + +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'; +import FormField from 'core-components/form-field'; +import Widget from 'core-components/widget'; +import Header from 'core-components/header'; + +class InviteUserWidget extends React.Component { + + static propTypes = { + onSuccess: React.PropTypes.func, + className: React.PropTypes.string + }; + + constructor(props) { + super(props); + + this.state = { + loading: false, + email: null, + customFields: [] + }; + } + + componentDidMount() { + API.call({ + path: '/system/get-custom-fields', + data: {} + }) + .then(result => this.setState({customFields: result.data})); + } + + render() { + return ( + +
+
+
+ + + {this.state.customFields.map(this.renderCustomField.bind(this))} +
+
+ +
+ {i18n('INVITE_USER')} + + + {this.renderMessage()} + + ); + } + + renderCustomField(customField, key) { + if(customField.type === 'text') { + return ( + + ); + } else { + const items = customField.options.map(option => ({content: option.name, value: option.name})); + + return ( + + ); + } + } + + renderMessage() { + switch (this.state.message) { + case 'success': + return {i18n('INVITE_USER_SUCCESS')}; + case 'fail': + return {i18n('EMAIL_EXISTS')}; + default: + return null; + } + } + + getClass() { + let classes = { + 'invite-user-widget': true, + [this.props.className]: this.props.className + }; + return classNames(classes); + } + + getFormProps() { + return { + loading: this.state.loading, + className: 'invite-user-widget__form', + onSubmit: this.onInviteUserFormSubmit.bind(this) + }; + } + + getInputProps(password) { + return { + className: 'invite-user-widget__input', + fieldProps: { + size: 'medium', + password: password + } + }; + } + + onInviteUserFormSubmit(formState) { + const captcha = this.refs.captcha.getWrappedInstance(); + + if (!captcha.getValue()) { + captcha.focus(); + } else { + this.setState({ + loading: true + }); + + const form = _.clone(formState); + + this.state.customFields.forEach(customField => { + if(customField.type === 'select') { + form[`customfield_${customField.name}`] = customField.options[form[`customfield_${customField.name}`]].name; + } + }) + + API.call({ + path: '/user/invite', + data: _.extend({captcha: captcha.getValue()}, form) + }).then(this.onInviteUserSuccess.bind(this)).catch(this.onInviteUserFail.bind(this)); + } + } + + onInviteUserSuccess() { + this.setState({ + loading: false, + message: 'success' + }); + } + + onInviteUserFail() { + this.setState({ + loading: false, + message: 'fail' + }); + } +} + +export default InviteUserWidget; diff --git a/client/src/app/admin/panel/users/invite-user-widget.scss b/client/src/app/admin/panel/users/invite-user-widget.scss new file mode 100644 index 00000000..9ecbf748 --- /dev/null +++ b/client/src/app/admin/panel/users/invite-user-widget.scss @@ -0,0 +1,19 @@ +.invite-user-widget { + padding: 30px; + text-align: center; + + &__form { + margin-bottom: 20px; + } + + &__inputs { + display: inline-block; + margin: 0 auto; + } + + &__captcha { + margin: 10px auto 20px; + height: 78px; + width: 304px; + } +} 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 6d30bae1..6fa35f4f 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 @@ -146,7 +146,7 @@ class CreateTicketForm extends React.Component { message: 'success' }, () => { if(this.props.onSuccess) { - this.props.onSuccess(); + this.props.onSuccess(result, email); } }); } 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 57e50d6c..a875f1fb 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 @@ -32,7 +32,7 @@ class DashboardCreateTicketPage extends React.Component { ); } - onCreateTicketSuccess() { + onCreateTicketSuccess(result, email) { if((this.props.location.pathname !== '/create-ticket')) { setTimeout(() => {history.push('/dashboard')}, 2000); } else { diff --git a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js index 6bf28bfb..75fbd39b 100644 --- a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js +++ b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js @@ -4,6 +4,7 @@ import _ from 'lodash'; import API from 'lib-app/api-call'; import i18n from 'lib-app/i18n'; +import { getCustomFieldParamName } from 'lib-core/APIUtils'; import SessionActions from 'actions/session-actions'; import AreYouSure from 'app-components/are-you-sure'; @@ -42,15 +43,6 @@ class DashboardEditProfilePage extends React.Component { return (
-
{i18n('ADDITIONAL_FIELDS')}
-
this.setState({customFieldsFrom: form})} onSubmit={this.onCustomFieldsSubmit.bind(this)}> -
- {this.state.customFields.map(this.renderCustomField.bind(this))} -
-
- {i18n('SAVE')} -
-
{i18n('EDIT_EMAIL')}
@@ -65,6 +57,23 @@ class DashboardEditProfilePage extends React.Component { {i18n('CHANGE_PASSWORD')} {this.renderMessagePass()} + {this.state.customFields.length ? this.renderCustomFields() : null} +
+ ); + } + + renderCustomFields() { + return ( +
+
{i18n('ADDITIONAL_FIELDS')}
+
this.setState({customFieldsFrom: form})} onSubmit={this.onCustomFieldsSubmit.bind(this)}> +
+ {this.state.customFields.map(this.renderCustomField.bind(this))} +
+
+ {i18n('SAVE')} +
+
); } @@ -116,9 +125,9 @@ class DashboardEditProfilePage extends React.Component { customFields.forEach(customField => { if(customField.type === 'select') { - parsedFrom[`customfield_${customField.name}`] = customField.options[form[customField.name]].name; + parsedFrom[getCustomFieldParamName(customField.name)] = customField.options[form[customField.name]].name; } else { - parsedFrom[`customfield_${customField.name}`] = form[customField.name]; + parsedFrom[getCustomFieldParamName(customField.name)] = form[customField.name]; } }); diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.js b/client/src/app/main/main-recover-password/main-recover-password-page.js index e8141c4e..a6807d52 100644 --- a/client/src/app/main/main-recover-password/main-recover-password-page.js +++ b/client/src/app/main/main-recover-password/main-recover-password-page.js @@ -30,7 +30,7 @@ class MainRecoverPasswordPage extends React.Component { render() { 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 b0634ea8..793090ca 100644 --- a/client/src/app/main/main-signup/main-signup-page.js +++ b/client/src/app/main/main-signup/main-signup-page.js @@ -1,7 +1,5 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import Widget from 'core-components/widget'; import MainSignUpWidget from 'app/main/main-signup/main-signup-widget'; class MainSignUpPage extends React.Component { @@ -9,70 +7,10 @@ class MainSignUpPage extends React.Component { render() { return (
- +
); } - - renderMessage() { - switch (this.state.message) { - case 'success': - return {i18n('SIGNUP_SUCCESS')}; - case 'fail': - return {i18n('EMAIL_EXISTS')}; - default: - return null; - } - } - - getFormProps() { - return { - loading: this.state.loading, - className: 'signup-widget__form', - onSubmit: this.onSignupFormSubmit.bind(this) - }; - } - - getInputProps(password) { - return { - className: 'signup-widget__input', - fieldProps: { - size: 'medium', - password: password - } - }; - } - - onSignupFormSubmit(formState) { - const captcha = this.refs.captcha.getWrappedInstance(); - - 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() { - this.setState({ - loading: false, - message: 'success' - }); - } - - onSignupFail() { - this.setState({ - loading: false, - message: 'fail' - }); - } } export default MainSignUpPage; diff --git a/client/src/app/main/main-signup/main-signup-widget.js b/client/src/app/main/main-signup/main-signup-widget.js index ce99dc3d..bfe390fe 100644 --- a/client/src/app/main/main-signup/main-signup-widget.js +++ b/client/src/app/main/main-signup/main-signup-widget.js @@ -1,10 +1,10 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import _ from 'lodash'; import classNames from 'classnames'; import i18n from 'lib-app/i18n'; import API from 'lib-app/api-call'; +import history from 'lib-app/history'; import Captcha from 'app/main/captcha'; import SubmitButton from 'core-components/submit-button'; @@ -17,7 +17,6 @@ import Header from 'core-components/header'; class MainSignUpWidget extends React.Component { static propTypes = { - onSuccess: React.PropTypes.func, className: React.PropTypes.string }; @@ -27,7 +26,7 @@ class MainSignUpWidget extends React.Component { this.state = { loading: false, email: null, - customFields: [] + customFields: null }; } @@ -40,6 +39,7 @@ class MainSignUpWidget extends React.Component { } render() { + if(!this.state.customFields) return null; return (
@@ -153,6 +153,8 @@ class MainSignUpWidget extends React.Component { this.setState({ loading: false, message: 'success' + }, () => { + setTimeout(() => {history.push('/check-ticket')}, 2000); }); } diff --git a/client/src/core-components/menu.js b/client/src/core-components/menu.js index 25cd805d..40ef6de2 100644 --- a/client/src/core-components/menu.js +++ b/client/src/core-components/menu.js @@ -140,4 +140,4 @@ class Menu extends React.Component { } } -export default Menu; \ No newline at end of file +export default Menu; diff --git a/client/src/core-components/menu.scss b/client/src/core-components/menu.scss index 64eab16c..d8b06c20 100644 --- a/client/src/core-components/menu.scss +++ b/client/src/core-components/menu.scss @@ -201,4 +201,4 @@ $transition: background-color 0.3s ease, color 0.3s ease; color: white; } } -} \ No newline at end of file +} diff --git a/client/src/core-components/pagination.js b/client/src/core-components/pagination.js index 4b4ad162..8ce28a8d 100644 --- a/client/src/core-components/pagination.js +++ b/client/src/core-components/pagination.js @@ -98,4 +98,4 @@ class Pagination extends React.Component { } } -export default Pagination; \ No newline at end of file +export default Pagination; diff --git a/client/src/core-components/table.js b/client/src/core-components/table.js index fe882f72..07c9f29f 100644 --- a/client/src/core-components/table.js +++ b/client/src/core-components/table.js @@ -55,7 +55,7 @@ class Table extends React.Component { 'table__header-column': true, [header.className]: (header.className) }; - + return (
); } @@ -139,15 +139,15 @@ class Table extends React.Component { this.props.onPageChange({target: {value: index}}); } } - + getRowClass(row) { let classes = { 'table__row': true, 'table__row-highlighted': row.highlighted }; - + classes[row.className] = (row.className); - + return classNames(classes); } @@ -167,4 +167,4 @@ class Table extends React.Component { } } -export default Table; \ No newline at end of file +export default Table; diff --git a/client/src/data/languages/br.js b/client/src/data/languages/br.js index 05dbb11f..12906cbf 100644 --- a/client/src/data/languages/br.js +++ b/client/src/data/languages/br.js @@ -300,7 +300,7 @@ export default { 'DELETE_USER_DESCRIPTION': 'O usuário não será capaz de entrar no sistema e todos os seus chamados serão apagados. Além disso, o e-mail não poderá mais ser usado.', 'DELETE_TOPIC_DESCRIPTION': 'Ao excluir o tópico, todos os artigos dele serão apagados.', 'EDIT_TOPIC_DESCRIPTION': 'Aqui você pode alterar o nome, o ícone ea cor do ícone do tópico.', - 'ADD_ARTICLE_DESCRIPTION': 'Aqui você pode adicionar um artigo que estará disponível para cada usuário. Ele será adicionado dentro da categoria {categoria}.', + 'ADD_ARTICLE_DESCRIPTION': 'Aqui você pode adicionar um artigo que estará disponível para cada usuário. Ele será adicionado dentro da categoria {category}.', 'LIST_ARTICLES_DESCRIPTION': 'Esta é uma lista de artigos que inclui informações sobre nossos serviços.', 'ADD_TOPIC_DESCRIPTION': 'Aqui você pode adicionar um tópico que funciona como uma categoria para artigos.', 'DELETE_ARTICLE_DESCRIPTION': 'Você vai excluir este artigo para sempre.', diff --git a/client/src/data/languages/de.js b/client/src/data/languages/de.js index 8ddfa7c9..cf126ee5 100644 --- a/client/src/data/languages/de.js +++ b/client/src/data/languages/de.js @@ -272,7 +272,7 @@ export default { 'INSTALLATION_COMPLETED': 'Installation abgeschlossen.', 'INSTALLATION_COMPLETED_DESCRIPTION': 'Die Installation von OpenSupports ist abgeschlossen. Umleitung zum Admin-Panel...', - 'STEP_TITLE': 'Schritt {aktuell} von {total} - {title}', + 'STEP_TITLE': 'Schritt {current} von {total} - {title}', 'STEP_1_DESCRIPTION': 'Wählen Sie Ihre bevorzugte Sprache für den Installationsassistenten aus.', 'STEP_2_DESCRIPTION': 'Hier sind die Voraussetzungen für das Ausführen von OpenSupports aufgelistet. Bitte stellen Sie sicher, dass alle Anforderungen erfüllt sind.', 'STEP_3_DESCRIPTION': 'Bitte füllen Sie die MySQL-Datenbankkonfiguration aus.', diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 6373f738..dc512fd5 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -11,6 +11,7 @@ export default { 'SIGN_UP': 'Sign up', 'FORGOT_PASSWORD': 'Forgot your password?', 'RECOVER_PASSWORD': 'Recover Password', + 'SET_UP_PASSWORD': 'Set up your password', 'RECOVER_SENT': 'An email with recover instructions has been sent.', 'NEW_EMAIL': 'New email', 'FULL_NAME': 'Full name', @@ -93,6 +94,7 @@ export default { 'CHANGE_EMAIL': 'Change email', 'CHANGE_PASSWORD': 'Change password', 'NAME': 'Name', + 'SEARCH': 'Search', 'SIGNUP_DATE': 'Sign up date', 'SEARCH_USERS': 'Search users...', 'SEARCH_EMAIL': 'Search email...', @@ -193,6 +195,8 @@ export default { 'NEVER': 'Never', 'HIMSELF': 'himself', 'ADD_USER': 'Add user', + 'INVITE_USER': 'Invite user', + 'INVITE_STAFF': 'Invite staff', 'UPLOAD_FILE': 'Upload file', 'PRIVATE': 'Private', 'ENABLE_USER': 'Enable User', @@ -201,7 +205,7 @@ export default { 'IMAGE_HEADER_URL': 'Image header URL', 'IMAGE_HEADER_DESCRIPTION': 'Image that will be used as header of the email', 'EMAIL_SETTINGS': 'Email Settings', - 'ADDITIONAL_FIELDS': 'Additonal Fields', + 'ADDITIONAL_FIELDS': 'Edit additonal fields', 'NEW_CUSTOM_FIELD': 'New Custom field', 'TYPE': 'Type', 'SELECT_INPUT': 'Select input', @@ -232,6 +236,7 @@ export default { 'ACTIVITY_EDIT_SETTINGS': 'edited settings', 'ACTIVITY_SIGNUP': 'signed up', + 'ACTIVITY_INVITE': 'invited user', 'ACTIVITY_ADD_TOPIC': 'added topic', 'ACTIVITY_ADD_ARTICLE': 'added article', 'ACTIVITY_DELETE_TOPIC': 'deleted topic', @@ -293,6 +298,7 @@ export default { 'MY_TICKETS_DESCRIPTION': 'Here you can view the tickets you are responsible for.', 'NEW_TICKETS_DESCRIPTION': 'Here you can view all the new tickets that are not assigned by anyone.', 'ALL_TICKETS_DESCRIPTION': 'Here you can view the tickets of the departments you are assigned.', + 'SEARCH_TICKETS_DESCRIPTION': 'Here you can search tickets by specific filters', 'TICKET_VIEW_DESCRIPTION': 'This ticket has been sent by a customer. Here you can respond or assign the ticket', 'BAN_USERS_DESCRIPTION': 'Here you can see a list of banned emails, you can un-ban them or add more emails to the list.', 'LIST_USERS_DESCRIPTION': 'This is the list of users that are registered in this platform. You can search for someone in particular, delete it or ban it.', @@ -322,7 +328,7 @@ export default { 'REGISTRATION_ENABLED': 'Registration has been enabled', 'ADD_API_KEY_DESCRIPTION': 'Insert the name and a registration api key will be generated.', 'SIGN_UP_VIEW_DESCRIPTION': 'Here you can create an account for our support center. It is required to send tickets and see documentation.', - 'EDIT_PROFILE_VIEW_DESCRIPTION': 'Here you can edit your user by changing your email or your password.', + 'EDIT_PROFILE_VIEW_DESCRIPTION': 'Here you can edit your user preferences.', 'ENABLE_USER_SYSTEM_DESCRIPTION': 'Enable/disable the use of an user system. If you disable it, all users will be deleted but the tickets will be kept. If you enable it, the users of existent tickets will be created.', 'CSV_DESCRIPTION': 'The CSV file must have 3 columns: email, password, name. There is no limit in row count. It will be created one user per row in the file.', 'SMTP_SERVER_DESCRIPTION': 'The configuration of the SMTP server allows the application to send mails. If you do not configure it, no emails will be sent by OpenSupports.', @@ -336,6 +342,8 @@ export default { 'IMAP_POLLING_DESCRIPTION': 'Inbox checking will not be done automatically by OpenSupports. You have to make POST requests periodically to this url to process the emails: {url}', 'NEW_CUSTOM_FIELD_DESCRIPTION': 'Here you can create a custom field for an user, it can be a blank text box or a fixed set of options.', 'CUSTOM_FIELDS_DESCRIPTION': 'Custom fields are defined additional fields the users are able to fill to provide more information about them.', + 'INVITE_USER_VIEW_DESCRIPTION': 'Here you can invite an user to join our support system, he will just need to provide his password to create a new user.', + 'INVITE_STAFF_DESCRIPTION': 'Here you can invite staff members to your teams.', //ERRORS 'EMAIL_OR_PASSWORD': 'Email or password invalid', @@ -372,6 +380,7 @@ export default { //MESSAGES 'SIGNUP_SUCCESS': 'You have registered successfully in our support system.', + 'INVITE_USER_SUCCESS': 'You have invited a new user successfully in our support system', 'TICKET_SENT': 'Ticket has been created successfully.', 'VALID_RECOVER': 'Password recovered successfully', 'EMAIL_EXISTS': 'Email already exists', diff --git a/client/src/data/languages/es.js b/client/src/data/languages/es.js index 35ad7f3d..89ddcf8f 100644 --- a/client/src/data/languages/es.js +++ b/client/src/data/languages/es.js @@ -206,7 +206,7 @@ export default { 'TYPE': 'Tipo', 'SELECT_INPUT': 'Seleccionar entrada', 'TEXT_INPUT': 'Entrada de texto', - 'OPTION': 'Opción {índice}', + 'OPTION': 'Opción {index}', 'OPTIONS': 'Opciones', 'FIELD_DESCRIPTION': 'Descripción del campo (opcional)', 'DESCRIPTION_ADD_CUSTOM_TAG': 'Aquí puedes agregar una nueva etiqueta personalizada', diff --git a/client/src/data/languages/fr.js b/client/src/data/languages/fr.js index c7356793..1877bca1 100644 --- a/client/src/data/languages/fr.js +++ b/client/src/data/languages/fr.js @@ -62,7 +62,7 @@ export default { 'HIGH': 'Haute', 'MEDIUM': 'Moyenne', 'LOW': 'Faible', - 'TITLE': 'Titre', + 'TITLE': 'Objet', 'CONTENT': 'Contenu', 'SAVE': 'Enregistrer', 'DISCARD_CHANGES': 'Annuler les modifications', @@ -343,7 +343,7 @@ export default { 'ERROR_EMPTY': 'Valeur invalide', 'ERROR_PASSWORD': 'Mot de passe incorrect', 'ERROR_NAME': 'Nom incorrect', - 'ERROR_TITLE': 'Titre incorrect', + 'ERROR_TITLE': 'Objet incorrect', 'ERROR_EMAIL': 'Email invalide', 'ERROR_CONTENT_SHORT': 'Contenu trop court', 'PASSWORD_NOT_MATCH': 'Le mot de passe ne correspond pas', diff --git a/client/src/data/languages/gr.js b/client/src/data/languages/gr.js index 5b66ffbc..939ab0b8 100644 --- a/client/src/data/languages/gr.js +++ b/client/src/data/languages/gr.js @@ -300,7 +300,7 @@ 'DELETE_USER_DESCRIPTION': 'Ο χρήστης δεν θα μπορέσει να συνδεθεί με τη γήρανση και όλα τα εισιτήρια του θα διαγραφούν. Επίσης, το ηλεκτρονικό ταχυδρομείο δεν μπορεί πλέον να χρησιμοποιηθεί.', 'DELETE_TOPIC_DESCRIPTION': 'Διαγράφοντας το θέμα, όλα τα άρθρα σε αυτό θα διαγραφούν.', 'EDIT_TOPIC_DESCRIPTION': 'Εδώ μπορείτε να αλλάξετε το όνομα, το εικονίδιο και το χρώμα του εικονιδίου του θέματος.', - 'ADD_ARTICLE_DESCRIPTION': 'Εδώ μπορείτε να προσθέσετε ένα άρθρο που θα είναι διαθέσιμο για κάθε χρήστη. Θα προστεθεί μέσα στην κατηγορία {κατηγορία}.', + 'ADD_ARTICLE_DESCRIPTION': 'Εδώ μπορείτε να προσθέσετε ένα άρθρο που θα είναι διαθέσιμο για κάθε χρήστη. Θα προστεθεί μέσα στην κατηγορία {category}.', 'LIST_ARTICLES_DESCRIPTION': 'Αυτή είναι μια λίστα με άρθρα που περιλαμβάνουν πληροφορίες σχετικά με τις υπηρεσίες μας.', 'ADD_TOPIC_DESCRIPTION': 'Εδώ μπορείτε να προσθέσετε ένα θέμα που λειτουργεί ως κατηγορία για άρθρα.', 'DELETE_ARTICLE_DESCRIPTION': 'Πρόκειται να διαγράψετε αυτό το άρθρο για πάντα.', diff --git a/client/src/data/languages/it.js b/client/src/data/languages/it.js index 53ba2917..3bc92519 100644 --- a/client/src/data/languages/it.js +++ b/client/src/data/languages/it.js @@ -206,7 +206,7 @@ export default { 'TYPE': 'genere', 'SELECT_INPUT': 'Seleziona input', 'TEXT_INPUT': 'L\'immissione di testo', - 'OPTION': 'Opzione {indice}', + 'OPTION': 'Opzione {index}', 'OPTIONS': 'Opzioni', 'FIELD_DESCRIPTION': 'Descrizione del campo (facoltativo)', 'DESCRIPTION_ADD_CUSTOM_TAG': 'qui puoi aggiungere un nuovo tag personalizzato', diff --git a/client/src/data/languages/pt.js b/client/src/data/languages/pt.js index 304dc99f..5ceff7f4 100644 --- a/client/src/data/languages/pt.js +++ b/client/src/data/languages/pt.js @@ -300,7 +300,7 @@ export default { 'DELETE_USER_DESCRIPTION': 'O usuário não será capaz de entrar no envelhecimento e todos os seus bilhetes serão apagados. Além disso, o e-mail não pode mais ser usado.', 'DELETE_TOPIC_DESCRIPTION': 'Ao excluir o tópico, todos os artigos dele serão apagados.', 'EDIT_TOPIC_DESCRIPTION': 'Aqui você pode alterar o nome, o ícone ea cor do ícone do tópico.', - 'ADD_ARTICLE_DESCRIPTION': 'Aqui você pode adicionar um artigo que estará disponível para cada usuário. Ele será adicionado dentro da categoria {categoria}.', + 'ADD_ARTICLE_DESCRIPTION': 'Aqui você pode adicionar um artigo que estará disponível para cada usuário. Ele será adicionado dentro da categoria {category}.', 'LIST_ARTICLES_DESCRIPTION': 'Esta é uma lista de artigos que inclui informações sobre nossos serviços.', 'ADD_TOPIC_DESCRIPTION': 'Aqui você pode adicionar um tópico que funciona como uma categoria para artigos.', 'DELETE_ARTICLE_DESCRIPTION': 'Você vai excluir este artigo para sempre.', diff --git a/client/src/index.html b/client/src/index.html index 90c8c2e2..a00af5b1 100755 --- a/client/src/index.html +++ b/client/src/index.html @@ -12,7 +12,6 @@
- diff --git a/client/src/lib-core/APIUtils.js b/client/src/lib-core/APIUtils.js index c9c544f5..d8ca183e 100644 --- a/client/src/lib-core/APIUtils.js +++ b/client/src/lib-core/APIUtils.js @@ -44,4 +44,8 @@ const APIUtils = { } }; +export const getCustomFieldParamName = function (customFieldName) { + return `customfield_${customFieldName}`.replace(/ /g,'_'); +} + export default APIUtils; diff --git a/server/controllers/staff.php b/server/controllers/staff.php index 997c5f25..1d3e3809 100755 --- a/server/controllers/staff.php +++ b/server/controllers/staff.php @@ -9,7 +9,7 @@ $systemControllerGroup->addController(new GetTicketStaffController); $systemControllerGroup->addController(new GetNewTicketsStaffController); $systemControllerGroup->addController(new GetAllTicketsStaffController); $systemControllerGroup->addController(new SearchTicketStaffController); -$systemControllerGroup->addController(new AddStaffController); +$systemControllerGroup->addController(new InviteStaffController); $systemControllerGroup->addController(new GetAllStaffController); $systemControllerGroup->addController(new DeleteStaffController); $systemControllerGroup->addController(new EditStaffController); diff --git a/server/controllers/staff/get-all-tickets.php b/server/controllers/staff/get-all-tickets.php index 93a30975..a1530e8f 100755 --- a/server/controllers/staff/get-all-tickets.php +++ b/server/controllers/staff/get-all-tickets.php @@ -50,7 +50,7 @@ class GetAllTicketsStaffController extends Controller { ]); return; } - + Response::respondSuccess([ 'tickets' => $this->getTicketList()->toArray(true), 'pages' => $this->getTotalPages() diff --git a/server/controllers/staff/get-all.php b/server/controllers/staff/get-all.php index 3b38daaf..b58e9875 100755 --- a/server/controllers/staff/get-all.php +++ b/server/controllers/staff/get-all.php @@ -11,7 +11,7 @@ use Respect\Validation\Validator as DataValidator; * * @apiDescription This path retrieves information about all the staff member. * - * @apiPermission staff3 + * @apiPermission staff1 * * @apiUse NO_PERMISSION * diff --git a/server/controllers/staff/add.php b/server/controllers/staff/invite.php similarity index 63% rename from server/controllers/staff/add.php rename to server/controllers/staff/invite.php index 65aa1b2e..28231ba4 100755 --- a/server/controllers/staff/add.php +++ b/server/controllers/staff/invite.php @@ -3,20 +3,19 @@ use Respect\Validation\Validator as DataValidator; DataValidator::with('CustomValidations', true); /** - * @api {post} /staff/add Add staff + * @api {post} /staff/invite Invite staff * @apiVersion 4.5.0 * - * @apiName Add staff + * @apiName Invite staff * * @apiGroup Staff * - * @apiDescription This path adds a new staff member. + * @apiDescription This path invites a new staff member. * * @apiPermission staff3 * * @apiParam {String} name The name of the new staff member. * @apiParam {String} email The email of the new staff member. - * @apiParam {String} password The password of the new staff member. * @apiParam {Number} level The level of the new staff member. * @apiParam {String} profilePic The profile pic of the new staff member. * @apiParam {Number[]} departments The departments that will have assigned the new staff member. @@ -33,18 +32,16 @@ DataValidator::with('CustomValidations', true); * */ -class AddStaffController extends Controller { - const PATH = '/add'; +class InviteStaffController extends Controller { + const PATH = '/invite'; const METHOD = 'POST'; private $name; private $email; - private $password; private $profilePic; private $level; private $departments; - public function validations() { return [ 'permission' => 'staff_3', @@ -57,53 +54,55 @@ class AddStaffController extends Controller { 'validation' => DataValidator::email(), 'error' => ERRORS::INVALID_EMAIL ], - 'password' => [ - 'validation' => DataValidator::length(5, 200), - 'error' => ERRORS::INVALID_PASSWORD - ], 'level' => [ 'validation' => DataValidator::between(1, 3, true), 'error' => ERRORS::INVALID_LEVEL ] - ] ]; } public function handler() { $this->storeRequestData(); + + $staffRow = Staff::getDataStore($this->email, 'email'); + + if(!$staffRow->isNull()) throw new RequestException(ERRORS::ALREADY_A_STAFF); + $staff = new Staff(); + $staff->setProperties([ + 'name'=> $this->name, + 'email' => $this->email, + 'password'=> Hashing::hashPassword(Hashing::generateRandomToken()), + 'profilePic' => $this->profilePic, + 'level' => $this->level, + 'sharedDepartmentList' => $this->getDepartmentList() + ]); - $staffRow = Staff::getDataStore($this->email,'email'); + $this->addOwner(); - if($staffRow->isNull()) { - $staff->setProperties([ - 'name'=> $this->name, - 'email' => $this->email, - 'password'=> Hashing::hashPassword($this->password), - 'profilePic' => $this->profilePic, - 'level' => $this->level, - 'sharedDepartmentList' => $this->getDepartmentList() + $this->token = Hashing::generateRandomToken(); - ]); - - $this->addOwner(); - - Log::createLog('ADD_STAFF', $this->name); - - Response::respondSuccess([ - 'id' => $staff->store() - ]); - return; - } + $recoverPassword = new RecoverPassword(); + $recoverPassword->setProperties(array( + 'email' => $this->email, + 'token' => $this->token, + 'staff' => true + )); + $recoverPassword->store(); - throw new RequestException(ERRORS::ALREADY_A_STAFF); + $this->sendInvitationMail(); + + Response::respondSuccess([ + 'id' => $staff->store() + ]); + + Log::createLog('INVITE', $this->name); } public function storeRequestData() { $this->name = Controller::request('name'); $this->email = Controller::request('email'); - $this->password = Controller::request('password'); $this->profilePic = Controller::request('profilePic'); $this->level = Controller::request('level'); $this->departments = Controller::request('departments'); @@ -120,6 +119,7 @@ class AddStaffController extends Controller { return $listDepartments; } + public function addOwner() { $departmentIds = json_decode($this->departments); @@ -129,4 +129,17 @@ class AddStaffController extends Controller { $departmentRow->store(); } } + + public function sendInvitationMail() { + $mailSender = MailSender::getInstance(); + + $mailSender->setTemplate(MailTemplate::USER_INVITE, [ + 'to' => $this->email, + 'name' => $this->name, + 'url' => Setting::getSetting('url')->getValue(), + 'token' => $this->token + ]); + + $mailSender->send(); + } } \ No newline at end of file diff --git a/server/controllers/ticket.php b/server/controllers/ticket.php index 5afbf8d5..e60c253e 100755 --- a/server/controllers/ticket.php +++ b/server/controllers/ticket.php @@ -23,5 +23,6 @@ $ticketControllers->addController(new DeleteTagController); $ticketControllers->addController(new GetTagsController); $ticketControllers->addController(new AddTagController); $ticketControllers->addController(new RemoveTagController); +$ticketControllers->addController(new SearchController); $ticketControllers->finalize(); diff --git a/server/controllers/ticket/comment.php b/server/controllers/ticket/comment.php index de6e2c85..6cd513a2 100755 --- a/server/controllers/ticket/comment.php +++ b/server/controllers/ticket/comment.php @@ -37,9 +37,10 @@ class CommentController extends Controller { private $ticket; private $content; + private $session; public function validations() { - $session = Session::getInstance(); + $this->session = Session::getInstance(); if (Controller::isUserSystemEnabled() || Controller::isStaffLogged()) { return [ @@ -64,11 +65,11 @@ class CommentController extends Controller { 'error' => ERRORS::INVALID_CONTENT ], 'ticketNumber' => [ - 'validation' => DataValidator::equals($session->getTicketNumber()), + 'validation' => DataValidator::equals($this->session->getTicketNumber()), 'error' => ERRORS::INVALID_TICKET ], 'csrf_token' => [ - 'validation' => DataValidator::equals($session->getToken()), + 'validation' => DataValidator::equals($this->session->getToken()), 'error' => ERRORS::INVALID_TOKEN ] ] @@ -79,28 +80,29 @@ class CommentController extends Controller { public function handler() { $this->requestData(); $ticketAuthor = $this->ticket->authorToArray(); - $isAuthor = $this->ticket->isAuthor(Controller::getLoggedUser()) || Session::getInstance()->isTicketSession(); - $isOwner = $this->ticket->isOwner(Controller::getLoggedUser()); - $user = Controller::getLoggedUser(); + $isAuthor = $this->session->isTicketSession() || $this->ticket->isAuthor($this->user); + $isOwner = $this->ticket->isOwner($this->user); + $private = Controller::request('private'); if(!Controller::isStaffLogged() && Controller::isUserSystemEnabled() && !$isAuthor){ throw new RequestException(ERRORS::NO_PERMISSION); } - if(!$user->canManageTicket($this->ticket)) { + if(!$this->session->isTicketSession() && !$this->user->canManageTicket($this->ticket)) { throw new RequestException(ERRORS::NO_PERMISSION); } $this->storeComment(); - if($isAuthor && $this->ticket->owner) { + if(!$isAuthor && !$private) { + $this->sendMail($ticketAuthor); + } + if($this->ticket->owner && !$isOwner) { $this->sendMail([ 'email' => $this->ticket->owner->email, 'name' => $this->ticket->owner->name, 'staff' => true ]); - } else if($isOwner) { - !Controller::request('private') ? $this->sendMail($ticketAuthor) : null; } Log::createLog('COMMENT', $this->ticket->ticketNumber); @@ -112,6 +114,7 @@ class CommentController extends Controller { $ticketNumber = Controller::request('ticketNumber'); $this->ticket = Ticket::getByTicketNumber($ticketNumber); $this->content = Controller::request('content', true); + $this->user = Controller::getLoggedUser(); } private function storeComment() { @@ -129,12 +132,14 @@ class CommentController extends Controller { )); if(Controller::isStaffLogged()) { - $this->ticket->unread = !$this->ticket->isAuthor(Controller::getLoggedUser()); - $this->ticket->unreadStaff = !$this->ticket->isOwner(Controller::getLoggedUser()); - $comment->authorStaff = Controller::getLoggedUser(); + $this->ticket->unread = !$this->ticket->isAuthor($this->user); + $this->ticket->unreadStaff = !$this->ticket->isOwner($this->user); + $comment->authorStaff = $this->user; } else if(Controller::isUserSystemEnabled()) { $this->ticket->unreadStaff = true; - $comment->authorUser = Controller::getLoggedUser(); + $comment->authorUser = $this->user; + } else { + $this->ticket->unreadStaff = true; } $this->ticket->addEvent($comment); diff --git a/server/controllers/ticket/create.php b/server/controllers/ticket/create.php index e3059498..6fb76b28 100755 --- a/server/controllers/ticket/create.php +++ b/server/controllers/ticket/create.php @@ -115,10 +115,16 @@ class CreateController extends Controller { } } - Log::createLog('CREATE_TICKET', $this->ticketNumber); Response::respondSuccess([ 'ticketNumber' => $this->ticketNumber ]); + + if(!Controller::isUserSystemEnabled() && !Controller::isStaffLogged()) { + $session = Session::getInstance(); + $session->createTicketSession($this->ticketNumber); + } + + Log::createLog('CREATE_TICKET', $this->ticketNumber); } private function storeTicket() { diff --git a/server/controllers/ticket/search.php b/server/controllers/ticket/search.php new file mode 100644 index 00000000..00431c34 --- /dev/null +++ b/server/controllers/ticket/search.php @@ -0,0 +1,348 @@ + 'staff_1', + 'requestData' => [ + 'page' => [ + 'validation' => DataValidator::oneOf(DataValidator::numeric()->positive(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_PAGE + ], + 'tags' => [ + 'validation' => DataValidator::oneOf(DataValidator::validTagsId(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_TAG_FILTER + ], + 'closed' => [ + 'validation' => DataValidator::oneOf(DataValidator::in(['0','1']),DataValidator::nullType()), + 'error' => ERRORS::INVALID_CLOSED_FILTER + ], + 'unreadStaff' => [ + 'validation' => DataValidator::oneOf(DataValidator::in(['0','1']),DataValidator::nullType()), + 'error' => ERRORS::INVALID_UNREAD_STAFF_FILTER + ], + 'priority' => [ + 'validation' => DataValidator::oneOf(DataValidator::validPriorities(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_PRIORITY_FILTER + ], + 'dateRange' => [ + 'validation' => DataValidator::oneOf(DataValidator::validDateRange(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_DATE_RANGE_FILTER + ], + 'departments' => [ + 'validation' => DataValidator::oneOf(DataValidator::validDepartmentsId(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_DEPARTMENT_FILTER + ], + 'authors' => [ + 'validation' => DataValidator::oneOf(DataValidator::validAuthorsId(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_AUTHOR_FILTER + ], + 'assigned' => [ + 'validation' => DataValidator::oneOf(DataValidator::in(['0','1']),DataValidator::nullType()), + 'error' => ERRORS::INVALID_ASSIGNED_FILTER + ], + 'orderBy' => [ + 'validation' => DataValidator::oneOf(DataValidator::validOrderBy(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_ORDER_BY + ], + ] + ]; + } + + public function handler() { + $inputs = [ + 'closed' => Controller::request('closed'), + 'tags' => json_decode(Controller::request('tags')), + 'unreadStaff' => Controller::request('unreadStaff'), + 'priority' => json_decode(Controller::request('priority')), + 'dateRange' => json_decode(Controller::request('dateRange')), + 'departments' => json_decode(Controller::request('departments')), + 'authors' => json_decode(Controller::request('authors'),true), + 'assigned' => Controller::request('assigned'), + 'query' => Controller::request('query'), + 'orderBy' => json_decode(Controller::request('orderBy'),true), + 'page' => Controller::request('page'), + 'allowedDepartments' => Controller::getLoggedUser()->sharedDepartmentList->toArray(), + ]; + + + $query = $this->getSQLQuery($inputs); + $queryWithOrder = $this->getSQLQueryWithOrder($inputs); + $totalCount = RedBean::getAll("SELECT COUNT(*) FROM (SELECT COUNT(*) " . $query . " ) AS T2", [':query' => $inputs['query']])[0]['COUNT(*)']; + $ticketIdList = RedBean::getAll($queryWithOrder, [':query' => "%" . $inputs['query'] . "%"]); + $ticketList = []; + + foreach ($ticketIdList as $item) { + $ticket = Ticket::getDataStore($item['id']); + array_push($ticketList, $ticket->toArray()); + } + $ticketTableExists = RedBean::exec("select table_name from information_schema.tables where table_name = 'ticket';"); + + if($ticketTableExists){ + Response::respondSuccess([ + 'tickets' => $ticketList, + 'pages' => ceil($totalCount / 10), + 'page' => $inputs['page'] ? ($inputs['page']*1) : 1 + ]); + }else{ + Response::respondSuccess([]); + } + + } + + public function getSQLQuery($inputs) { + $tagsTableExists = RedBean::exec("select table_name from information_schema.tables where table_name = 'tag_ticket';"); + $ticketEventTableExists = RedBean::exec("select table_name from information_schema.tables where table_name = 'ticketevent';"); + + $taglistQuery = ( $tagsTableExists ? " LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id" : ''); + $ticketeventlistQuery = ( $ticketEventTableExists ? " LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id" : ''); + + $query = "FROM (ticket" . $taglistQuery . $ticketeventlistQuery .")"; + $filters = ""; + $this->setQueryFilters($inputs, $filters); + $query .= $filters . " GROUP BY ticket.id"; + return $query; + } + + public function getSQLQueryWithOrder($inputs) { + $query = $this->getSQLQuery($inputs); + $order = ""; + $query = "SELECT" . " ticket.id " . $query; + + $this->setQueryOrder($inputs, $order); + $inputs['page'] ? $page = $inputs['page'] : $page = 1 ; + $query .= $order ." LIMIT 10 OFFSET " . (($page-1)*10); + return $query; + } + + //FILTER + private function setQueryFilters($inputs, &$filters){ + if(array_key_exists('tags',$inputs)) $this->setTagFilter($inputs['tags'], $filters); + if(array_key_exists('closed',$inputs)) $this->setClosedFilter($inputs['closed'], $filters); + if(array_key_exists('assigned',$inputs)) $this->setAssignedFilter($inputs['assigned'], $filters); + if(array_key_exists('unreadStaff',$inputs)) $this->setSeenFilter($inputs['unreadStaff'], $filters); + if(array_key_exists('priority',$inputs)) $this->setPriorityFilter($inputs['priority'], $filters); + if(array_key_exists('dateRange',$inputs)) $this->setDateFilter($inputs['dateRange'], $filters); + if(array_key_exists('departments',$inputs)) $this->setDepartmentFilter($inputs['departments'],$inputs['allowedDepartments'], $filters); + if(array_key_exists('authors',$inputs)) $this->setAuthorFilter($inputs['authors'], $filters); + if(array_key_exists('query',$inputs)) $this->setStringFilter($inputs['query'], $filters); + if($filters != "") $filters = " WHERE " . $filters; + } + + private function setTagFilter($tagList, &$filters){ + $tagsTableExists = RedBean::exec("select table_name from information_schema.tables where table_name = 'tag_ticket';"); + + if($tagList && $tagsTableExists){ + $filters != "" ? $filters .= " and " : null; + + foreach($tagList as $key => $tag) { + + $key == 0 ? $filters .= " ( " : null; + ($key != 0 && $key != sizeof($tagList)) ? $filters .= " or " : null; + + $filters .= "tag_ticket.tag_id = " . $tag ; + } + $filters .= ")"; + } + } + public function setClosedFilter($closed, &$filters){ + if ($closed !== null) { + if ($filters != "") $filters .= " and "; + $filters .= "ticket.closed = " . $closed ; + } + } + private function setSeenFilter($unreadStaff, &$filters){ + if ($unreadStaff !== null) { + if ($filters != "") $filters .= " and "; + $filters .= "ticket.unread_staff = " . $unreadStaff; + } + } + private function setPriorityFilter($priorities, &$filters){ + if($priorities !== null){ + $first = TRUE; + if ($filters != "") $filters .= " and "; + foreach(array_unique($priorities) as $priority) { + + if($first){ + $filters .= " ( "; + $first = FALSE; + } else { + $filters .= " or "; + } + + if($priority == 0){ + $filters .= "ticket.priority = 'low'"; + }elseif($priority == 1){ + $filters .= "ticket.priority = 'medium'"; + }elseif($priority == 2){ + $filters .= "ticket.priority = 'high'"; + } + + + } + $priorities != "" ? $filters .= ") " : null; + } + + } + + private function setDateFilter($dateRange, &$filters){ + if ($dateRange !== null) { + if ($filters != "") $filters .= " and "; + + foreach($dateRange as $key => $date) { + $key == 0 ? ($filters .= "(ticket.date >= " . $date ): ($filters .= " and ticket.date <= " . $date . ")"); + } + } + } + + private function setDepartmentFilter($departments,$allowedDepartments, &$filters){ + $validDepartments = $this->generateValidDepartmentList($departments, $allowedDepartments); + if ($filters != "") $filters .= " and "; + $first = TRUE; + + foreach($validDepartments as $department) { + if($first){ + $filters .= " ( "; + $first = FALSE; + } else { + $filters .= " or "; + } + $filters .= "ticket.department_id = " . $department; + } + $filters .= ")"; + + } + + private function setAuthorFilter($authors, &$filters){ + if($authors !== null){ + $first = TRUE; + if ($filters != "") $filters .= " and "; + + foreach($authors as $author){ + + if($first){ + $filters .= " ( "; + $first = FALSE; + } else { + $filters .= " or "; + } + + if($author['staff']){ + $filters .= "ticket.author_staff_id = " . $author['id']; + } else { + $filters .= "ticket.author_id = " . $author['id']; + } + } + + $filters .= ")"; + + } + } + + private function setAssignedFilter($assigned, &$filters){ + if($assigned !== null){ + if ($filters != "") $filters .= " and "; + $key = ""; + $assigned == 0 ? $key = "IS NULL" : $key = "IS NOT NULL"; + $filters .= "ticket.owner_id " . $key; + } + } + + private function setStringFilter($search, &$filters){ + $ticketEventTableExists = RedBean::exec("select table_name from information_schema.tables where table_name = 'ticketevent';"); + + if($search !== null){ + if ($filters != "") $filters .= " and "; + $ticketevent = ( $ticketEventTableExists ? " or (ticketevent.type = 'COMMENT' and ticketevent.content LIKE :query)" : ""); + $filters .= " (ticket.title LIKE :query or ticket.content LIKE :query or ticket.ticket_number LIKE :query". $ticketevent ." )"; + }; + } + + private function generateValidDepartmentList($departments, $allowedDepartments){ + $result = []; + $managedDepartments = []; + if($departments == null) $departments = []; + foreach ($allowedDepartments as $department) { + array_push($managedDepartments,$department['id']); + } + $result = array_intersect($departments,$managedDepartments); + + if(empty($result)) $result = $managedDepartments; + + $result = array_unique($result); + + return $result; + } + + //ORDER + private function setQueryOrder($inputs, &$order){ + $order = " ORDER BY "; + if(array_key_exists('query',$inputs)) $this->setStringOrder($inputs['query'], $order); + if(array_key_exists('orderBy',$inputs)) $this->setEspecificOrder($inputs['orderBy'], $order); + $order .= "ticket.closed asc, ticket.owner_id asc, ticket.unread_staff asc, ticket.priority desc, ticket.date desc "; + } + private function setEspecificOrder($orderBy, &$order){ + if($orderBy !== null){ + $orientation = ($orderBy['asc'] ? " asc" : " desc" ); + $order .= "ticket." . $orderBy['value'] . $orientation . ","; + }; + } + private function setStringOrder($querysearch, &$order){ + $ticketEventTableExists = RedBean::exec("select table_name from information_schema.tables where table_name = 'ticketevent';"); + + if($querysearch !== null){ + $ticketeventOrder = ( $ticketEventTableExists ? " CASE WHEN (ticketevent.type = 'COMMENT' and ticketevent.content LIKE :query) THEN ticketevent.content END desc," : ""); + $order .= "CASE WHEN (ticket.ticket_number LIKE :query) THEN ticket.ticket_number END desc,CASE WHEN (ticket.title LIKE :query) THEN ticket.title END desc, CASE WHEN ( ticket.content LIKE :query) THEN ticket.content END desc," . $ticketeventOrder ; + } + } + +} diff --git a/server/controllers/user.php b/server/controllers/user.php index d5d08020..feb48483 100755 --- a/server/controllers/user.php +++ b/server/controllers/user.php @@ -4,6 +4,7 @@ $userControllers->setGroupPath('/user'); $userControllers->addController(new LoginController); $userControllers->addController(new SignUpController); +$userControllers->addController(new InviteUserController); $userControllers->addController(new LogoutController); $userControllers->addController(new CheckSessionController); $userControllers->addController(new SendRecoverPasswordController); diff --git a/server/controllers/user/invite.php b/server/controllers/user/invite.php new file mode 100755 index 00000000..469829d5 --- /dev/null +++ b/server/controllers/user/invite.php @@ -0,0 +1,140 @@ + 'staff_1', + 'requestData' => [ + 'name' => [ + 'validation' => DataValidator::length(2, 55), + 'error' => ERRORS::INVALID_NAME + ], + 'email' => [ + 'validation' => DataValidator::email(), + 'error' => ERRORS::INVALID_EMAIL + ] + ] + ]; + + $validations['requestData']['captcha'] = [ + 'validation' => DataValidator::captcha(), + 'error' => ERRORS::INVALID_CAPTCHA + ]; + + return $validations; + } + + public function handler() { + if (!Controller::isUserSystemEnabled()) { + throw new RequestException(ERRORS::USER_SYSTEM_DISABLED); + } + + $this->storeRequestData(); + + $existentUser = User::getUser($this->userEmail, 'email'); + + if (!$existentUser->isNull()) { + throw new RequestException(ERRORS::USER_EXISTS); + } + + $banRow = Ban::getDataStore($this->userEmail, 'email'); + + if (!$banRow->isNull()) { + throw new RequestException(ERRORS::ALREADY_BANNED); + } + + $userId = $this->createNewUserAndRetrieveId(); + + $this->token = Hashing::generateRandomToken(); + + $recoverPassword = new RecoverPassword(); + $recoverPassword->setProperties(array( + 'email' => $this->userEmail, + 'token' => $this->token, + 'staff' => false + )); + $recoverPassword->store(); + + $this->sendInvitationMail(); + + Response::respondSuccess([ + 'userId' => $userId, + 'userEmail' => $this->userEmail + ]); + + Log::createLog('INVITE', $this->userName); + } + + public function storeRequestData() { + $this->userName = Controller::request('name'); + $this->userEmail = Controller::request('email'); + } + + public function createNewUserAndRetrieveId() { + $userInstance = new User(); + + $userInstance->setProperties([ + 'name' => $this->userName, + 'signupDate' => Date::getCurrentDate(), + 'tickets' => 0, + 'email' => $this->userEmail, + 'password' => Hashing::hashPassword(Hashing::generateRandomToken()), + 'verificationToken' => null, + 'xownCustomfieldvalueList' => $this->getCustomFieldValues() + ]); + + return $userInstance->store(); + } + + public function sendInvitationMail() { + $mailSender = MailSender::getInstance(); + + $mailSender->setTemplate(MailTemplate::USER_INVITE, [ + 'to' => $this->userEmail, + 'name' => $this->userName, + 'url' => Setting::getSetting('url')->getValue(), + 'token' => $this->token + ]); + + $mailSender->send(); + } +} diff --git a/server/controllers/user/recover-password.php b/server/controllers/user/recover-password.php index 9e712493..077f52ed 100755 --- a/server/controllers/user/recover-password.php +++ b/server/controllers/user/recover-password.php @@ -56,10 +56,6 @@ class RecoverPasswordController extends Controller { } public function handler() { - if(!Controller::isUserSystemEnabled()) { - throw new RequestException(ERRORS::USER_SYSTEM_DISABLED); - } - $this->requestData(); $this->changePassword(); } @@ -69,30 +65,38 @@ class RecoverPasswordController extends Controller { $this->token = Controller::request('token'); $this->password = Controller::request('password'); } + public function changePassword() { $recoverPassword = RecoverPassword::getDataStore($this->token, 'token'); + if($recoverPassword->isNull() || $recoverPassword->email !== $this->email) { + throw new RequestException(ERRORS::NO_PERMISSION); + } + + if(!Controller::isUserSystemEnabled() && !$recoverPassword->staff) { + throw new RequestException(ERRORS::USER_SYSTEM_DISABLED); + } + if($recoverPassword->staff) { $this->user = Staff::getDataStore($this->email, 'email'); - }else { + } else { $this->user = User::getDataStore($this->email, 'email'); } - if (!$recoverPassword->isNull() && !$this->user->isNull()) { - $recoverPassword->delete(); + if($this->user->isNull()) throw new RequestException(ERRORS::NO_PERMISSION); - $this->user->setProperties([ - 'password' => Hashing::hashPassword($this->password) - ]); + $recoverPassword->delete(); - $this->user->store(); + $this->user->setProperties([ + 'password' => Hashing::hashPassword($this->password) + ]); - $this->sendMail(); - Response::respondSuccess(['staff' => $recoverPassword->staff]); - } else { - throw new RequestException(ERRORS::NO_PERMISSION); - } + $this->user->store(); + + $this->sendMail(); + Response::respondSuccess(['staff' => $recoverPassword->staff]); } + public function sendMail() { $mailSender = MailSender::getInstance(); diff --git a/server/controllers/user/send-recover-password.php b/server/controllers/user/send-recover-password.php index 3ba9d2b6..8d925799 100755 --- a/server/controllers/user/send-recover-password.php +++ b/server/controllers/user/send-recover-password.php @@ -49,17 +49,18 @@ class SendRecoverPasswordController extends Controller { } public function handler() { - if(!Controller::isUserSystemEnabled()) { + $this->staff = Controller::request('staff'); + + if(!Controller::isUserSystemEnabled() && !$this->staff) { throw new RequestException(ERRORS::USER_SYSTEM_DISABLED); } - $this->staff = Controller::request('staff'); $email = Controller::request('email'); if($this->staff){ - $this->user = Staff::getUser($email,'email'); - }else { - $this->user = User::getUser($email,'email'); + $this->user = Staff::getUser($email, 'email'); + } else { + $this->user = User::getUser($email, 'email'); } if(!$this->user->isNull()) { diff --git a/server/controllers/user/signup.php b/server/controllers/user/signup.php index 0bc8ec5a..12184a0b 100755 --- a/server/controllers/user/signup.php +++ b/server/controllers/user/signup.php @@ -18,7 +18,7 @@ DataValidator::with('CustomValidations', true); * @apiParam {String} name The name of the new user. * @apiParam {String} email The email of the new user. * @apiParam {String} password The password of the new user. - * @apiParam {String} apiKey APIKey to sign up an user if the user system is disabled. + * @apiParam {String} apiKey APIKey to sign up an user if the registration system is disabled. * @apiParam {String} customfield_ Custom field values for this user. * * @apiUse INVALID_NAME diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index 9406ce80..34338349 100755 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -281,6 +281,15 @@ class ERRORS { const INVALID_PRIORITY = 'INVALID_PRIORITY'; const INVALID_PAGE = 'INVALID_PAGE'; const INVALID_QUERY = 'INVALID_QUERY'; + const INVALID_TAG_FILTER = 'INVALID_TAG_FILTER'; + const INVALID_CLOSED_FILTER = 'INVALID_CLOSED_FILTER'; + const INVALID_UNREAD_STAFF_FILTER = 'INVALID_UNREAD_STAFF_FILTER'; + const INVALID_PRIORITY_FILTER = 'INVALID_PRIORITY_FILTER'; + const INVALID_DATE_RANGE_FILTER = 'INVALID_DATE_RANGE_FILTER'; + const INVALID_DEPARTMENT_FILTER = 'INVALID_DEPARTMENT_FILTER'; + const INVALID_AUTHOR_FILTER = 'INVALID_AUTHOR_FILTER'; + const INVALID_ASSIGNED_FILTER = 'INVALID_ASSIGNED_FILTER'; + const INVALID_ORDER_BY = 'INVALID_ORDER_BY'; const INVALID_TOPIC = 'INVALID_TOPIC'; const INVALID_SEARCH = 'INVALID_SEARCH'; const INVALID_ORDER = 'INVALID_ORDER'; diff --git a/server/data/MailTexts.php b/server/data/MailTexts.php index c30e4d8d..527e14cd 100644 --- a/server/data/MailTexts.php +++ b/server/data/MailTexts.php @@ -25,6 +25,12 @@ class MailTexts { 'Hi, {{name}}. You have requested to recover your password.', 'Use this code in {{url}}/recover-password?email={{to}}&token={{token}} or click the button below.', ], + 'USER_INVITE' => [ + 'You have been invited - OpenSupports', + 'You have been invited', + 'Hi, {{name}}. You have been invited to join our support center.', + 'Use this code in {{url}}/recover-password?email={{to}}&token={{token}}&invited=true or click the button below to set up your password.' + ], 'USER_SYSTEM_DISABLED' => [ 'Access system changed - OpenSupports', 'Access system changed', @@ -85,6 +91,12 @@ class MailTexts { '喂 {{name}}。 您已要求恢复密码。', '使用此代码 {{url}}/recover-password?email={{to}}&token={{token}} 或单击下面的按钮.', ], + 'USER_INVITE' => [ + '您已受邀 - OpenSupports', + '您已受邀', + '你好, {{name}}. 邀请您加入我们的支持中心.', + '使用此代码 {{url}}/recover-password?email={{to}}&token={{token}}&invited=true 或单击下面的按钮来设置密码.' + ], 'USER_SYSTEM_DISABLED' => [ '访问系统更改 - OpenSupports', '访问系统更改', @@ -145,6 +157,12 @@ class MailTexts { 'Hallo, {{name}}. Sie haben aufgefordert, Ihr Passwort wiederherzustellen.', 'Verwenden Sie diesen Code in {{url}}/recover-password?email={{to}}&token={{token}} oder klicken Sie auf die Schaltfläche unten.', ], + 'USER_INVITE' => [ + 'Du bist eingeladen - OpenSupports', + 'Du bist eingeladen', + 'Hallo, {{name}}. Sie wurden zu unserem Support-Center eingeladen.', + 'Verwenden Sie diesen Code in {{url}}/recover-password?email={{to}}&token={{token}}&invited=true oder klicken Sie auf die Schaltfläche unten, um Ihr Passwort einzurichten.' + ], 'USER_SYSTEM_DISABLED' => [ 'Access system changed - OpenSupports', 'Zugriffssystem geändert', @@ -205,6 +223,12 @@ class MailTexts { 'Hola, {{name}}. Has requerido recuperar tu contraseña.', 'Usá este codigo en {{url}}/recover-password?email={{to}}&token={{token}} o hacé click en el botón de abajo.', ], + 'USER_INVITE' => [ + 'Haz sido invitado - OpenSupports', + 'Haz sido invitado', + 'Hola, {{name}}. Haz sido invitado a unirte a nuestro sistema de soporte.', + 'Usa este código en {{url}}/recover-password?email={{to}}&token={{token}}&invited=true o haz click en el botón de abajo para establecer tu contraseña.' + ], 'USER_SYSTEM_DISABLED' => [ 'Sistema de acceso cambiado - OpenSupports', 'Sistema de acceso cambiado', @@ -265,6 +289,12 @@ class MailTexts { 'Salut, {{name}}. Vous avez demandé à récupérer votre mot de passe.', 'Utilisez ce code dans {{url}}/recover-password?email={{to}}&token={{token}} ou cliquez sur le bouton ci-dessous.', ], + 'USER_INVITE' => [ + 'You have been invited - OpenSupports', + 'You have been invited', + 'Hi, {{name}}. You have been invited to join our support center.', + 'Use this code in {{url}}/recover-password?email={{to}}&token={{token}}&invited=true or click the button below to set up your password.' + ], 'USER_SYSTEM_DISABLED' => [ 'Système d\'accès modifié - OpenSupports', 'Système d\'accès modifié', @@ -325,6 +355,12 @@ class MailTexts { 'नमस्ते {{name}}. आपने अपना पासवर्ड पुनर्प्राप्त करने का अनुरोध किया है', 'इस कोड का उपयोग करें {{url}}/recover-password?email={{to}}&token={{token}} या नीचे दिए गए बटन पर क्लिक करें.', ], + 'USER_INVITE' => [ + 'आपको आमंत्रित किया गया है - OpenSupports', + 'आपको आमंत्रित किया गया है', + 'नमस्ते, {{name}}. आपको हमारे सहायता केंद्र से जुड़ने के लिए आमंत्रित किया गया है.', + 'इस कोड का उपयोग करें {{url}}/recover-password?email={{to}}&token={{token}}&invited=true या अपना पासवर्ड सेट करने के लिए नीचे दिए गए बटन पर क्लिक करें.' + ], 'USER_SYSTEM_DISABLED' => [ 'sistem akses berubah - OpenSupports', 'एक्सेस सिस्टम बदल गया', @@ -385,6 +421,12 @@ class MailTexts { 'Ciao, {{name}}. Hai richiesto di recuperare la tua password.', 'Clicca sul link {{url}}/recover-password?email={{to}}&token={{token}} o clicca sul pulsante qui sotto.', ], + 'USER_INVITE' => [ + 'Sei stato invitato - OpenSupports', + 'Sei stato invitato', + 'Ciao, {{name}}. Sei stato invitato a far parte del nostro centro di supporto.', + 'Usa questo codice in {{url}}/recover-password?email={{to}}&token={{token}}&invited=true oppure fai clic sul pulsante in basso per impostare la password.' + ], 'USER_SYSTEM_DISABLED' => [ 'Il sistema di accesso è cambiato - OpenSupports', 'Modifica sistema di accesso', @@ -445,6 +487,12 @@ class MailTexts { 'こんにちは、{{name}}。 パスワードの回復を要求しました。', 'でこのコードを使用 {{url}}/recover-password?email={{to}}&token={{token}} 下のボタンをクリックしてください.', ], + 'USER_INVITE' => [ + '招待されました - OpenSupports', + '招待されました', + 'こんにちは, {{name}}. サポートセンターに招待されました.', + 'このコードを {{url}}/recover-password?email={{to}}&token={{token}}&invited=true または、下のボタンをクリックしてパスワードを設定します.' + ], 'USER_SYSTEM_DISABLED' => [ 'アクセスシステムが変更されました - OpenSupports', 'アクセスシステムが変更されました', @@ -505,6 +553,12 @@ class MailTexts { 'Olá, {{name}}. Você solicitou a recuperação da sua senha.', 'Use este código em {{url}}/recover-password?email={{to}}&token={{token}} ou clique no botão abaixo.', ], + 'USER_INVITE' => [ + 'Você foi convidado - OpenSupports', + 'Você foi convidado', + 'Oi, {{name}}. Você foi convidado a participar do nosso centro de suporte.', + 'Use este código em {{url}}/recover-password?email={{to}}&token={{token}}&invited=true ou clique no botão abaixo para configurar sua senha.' + ], 'USER_SYSTEM_DISABLED' => [ 'Sistema de acesso alterado - OpenSupports', 'Sistema de acesso alterado', @@ -565,6 +619,12 @@ class MailTexts { 'Здравствуй, {{name}}. Вы запросили восстановить пароль.', 'Используйте этот код в {{url}}/recover-password?email={{to}}&token={{token}} или нажмите кнопку ниже.', ], + 'USER_INVITE' => [ + 'Вы были приглашены - OpenSupports', + 'Вы были приглашены', + 'Здравствуй, {{name}}. Вас пригласили присоединиться к нашему центру поддержки.', + 'Используйте этот код в {{url}}/recover-password?email={{to}}&token={{token}}&invited=true или нажмите кнопку ниже, чтобы установить пароль.' + ], 'USER_SYSTEM_DISABLED' => [ 'Система доступа изменена - OpenSupports', 'Система доступа изменена', @@ -625,6 +685,12 @@ class MailTexts { 'Merhaba, {{name}}. Şifrenizi geri yüklemenizi istediniz.', 'Bu kodu şu adreste kullanın {{url}}/recover-password?email={{to}}&token={{token}} veya aşağıdaki butona tıklayın.', ], + 'USER_INVITE' => [ + 'Davet edildin - OpenSupports', + 'Davet edildin', + 'Merhaba, {{name}}. Destek merkezimize katılmaya davet edildiniz.', + 'Bu kodu {{url}}/recover-password?email={{to}}&token={{token}}&invited=true veya şifrenizi ayarlamak için aşağıdaki butona tıklayın.' + ], 'USER_SYSTEM_DISABLED' => [ 'Erişim sistemi değiştirildi - OpenSupports', 'Erişim sistemi değiştirildi', @@ -685,6 +751,12 @@ class MailTexts { 'Olá, {{name}}. Você solicitou a recuperação da sua senha.', 'Use este código em {{url}}/recover-password?email={{to}}&token={{token}} ou clique no botão abaixo.', ], + 'USER_INVITE' => [ + 'Você foi convidado - OpenSupports', + 'Você foi convidado', + 'Oi, {{name}}. Você foi convidado a participar do nosso centro de suporte.', + 'Use este código em {{url}}/recover-password?email={{to}}&token={{token}}&invited=true ou clique no botão abaixo para configurar sua senha.' + ], 'USER_SYSTEM_DISABLED' => [ 'Sistema de acesso alterado - OpenSupports', 'Sistema de acesso alterado', @@ -745,6 +817,12 @@ class MailTexts { 'Γεια σου, {{name}}. Ζητήσατε να ανακτήσετε τον κωδικό πρόσβασής σας.', 'Χρησιμοποιήστε αυτόν τον κωδικό στο {{url}} / recover-password? Email = {{to}} & token = {{token}} ή κάντε κλικ στο παρακάτω κουμπί.', ], + 'USER_INVITE' => [ + 'Έχετε προσκληθεί - OpenSupports', + 'Έχετε προσκληθεί', + 'Γεια σου, {{name}}. Έχετε προσκληθεί να συμμετάσχετε στο κέντρο υποστήριξής μας.', + 'Χρησιμοποιήστε αυτόν τον κωδικό στο {{url}}/recover-password?email={{to}}&token={{token}}&invited=true ή κάντε κλικ στο παρακάτω κουμπί για να ρυθμίσετε τον κωδικό πρόσβασής σας.' + ], 'USER_SYSTEM_DISABLED' => [ 'Το σύστημα πρόσβασης άλλαξε - OpenSupports', 'Το σύστημα πρόσβασης άλλαξε', @@ -805,6 +883,12 @@ class MailTexts { 'Hallo, {{name}}. U heeft een verzoek gedaan om uw wachtwoord te resetten.', 'Gebruik deze code {{url}}/recover-password?email={{to}}&token={{token}} of klik op de knop hieronder.' ], + 'USER_INVITE' => [ + 'Je bent uitgenodigd - OpenSupports', + 'Je bent uitgenodigd', + 'Hallo, {{name}}. U bent uitgenodigd om lid te worden van ons ondersteuningscentrum.', + 'Gebruik deze code in {{url}}/recover-password?email={{to}}&token={{token}}&invited=true of klik op de onderstaande knop om uw wachtwoord in te stellen.' + ], 'USER_SYSTEM_DISABLED' => [ 'Toegangssysteem gewijzigd - OpenSupports', 'Toegang tot incidenten is gewijzigd', @@ -865,6 +949,12 @@ class MailTexts { 'Hej, {{name}}. Zażądałeś odzyskania hasła.', 'Użyj tego linka {{url}}/recover-password?email={{to}}&token={{token}} lub kliknij przycisk poniżej.', ], + 'USER_INVITE' => [ + 'Zostałeś zaproszony - OpenSupports', + 'Zostałeś zaproszony', + 'Hej, {{name}}. Zaproszono Cię do dołączenia do naszego centrum wsparcia.', + 'Użyj tego kodu w {{url}}/recover-password?email={{to}}&token={{token}}&invited=true lub kliknij przycisk poniżej, aby ustawić hasło.' + ], 'USER_SYSTEM_DISABLED' => [ 'Zmieniono dostęp do systemu - OpenSupports', 'Zmieniono dostęp do systemu', diff --git a/server/data/mail-templates/user-invite.html b/server/data/mail-templates/user-invite.html new file mode 100755 index 00000000..acc07035 --- /dev/null +++ b/server/data/mail-templates/user-invite.html @@ -0,0 +1,384 @@ + + + + + + Support Center + + + + + + + + + + + +
{header.value} @@ -97,7 +97,7 @@ class Table extends React.Component { }; return ( - + {row[key]}
+ + + + + + + + + +
+
+ + + + +
+
+ + + + +
+ logo +
+
+ +
+
+
+
+ + + + + + + + + + +
+ {{USER_INVITE_MATCH_1}} +
+ {{USER_INVITE_MATCH_2}} +
+ + + + +
+ + + + + + + + + + +
+ {{USER_INVITE_MATCH_3}} +
+ {{token}} +
+ +
+
+
+
+
+
+ + + + +
+ OpenSupports
+ Open source ticket system
+ www.opensupports.com

+
+
+
+ + diff --git a/server/libs/Controller.php b/server/libs/Controller.php index e8c61ff2..04711b5f 100755 --- a/server/libs/Controller.php +++ b/server/libs/Controller.php @@ -152,9 +152,8 @@ abstract class Controller { public static function getCustomFieldValues() { $customFields = Customfield::getAll(); $customFieldValues = new DataStoreList(); - foreach($customFields as $customField) { - $value = Controller::request('customfield_' . $customField->name); + $value = Controller::request('customfield_' . str_replace(' ', '_', $customField->name)); if($value !== null) { $customFieldValue = new Customfieldvalue(); $customFieldValue->setProperties([ @@ -183,7 +182,6 @@ abstract class Controller { $customFieldValues->add($customFieldValue); } } - return $customFieldValues; } } diff --git a/server/libs/validations/validAuthorsId.php b/server/libs/validations/validAuthorsId.php new file mode 100644 index 00000000..3cda8229 --- /dev/null +++ b/server/libs/validations/validAuthorsId.php @@ -0,0 +1,23 @@ +staff){ + $author = \Staff::getDataStore($authorObject->id); + }else{ + $author = \User::getDataStore($authorObject->id); + } + if($author->isNull()) return false; + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/server/libs/validations/validDateRange.php b/server/libs/validations/validDateRange.php new file mode 100644 index 00000000..c2bb3f05 --- /dev/null +++ b/server/libs/validations/validDateRange.php @@ -0,0 +1,20 @@ +isNull()) return false; + } + return true; + } + return false; + } +} diff --git a/server/libs/validations/validOrderBy.php b/server/libs/validations/validOrderBy.php new file mode 100644 index 00000000..40babc27 --- /dev/null +++ b/server/libs/validations/validOrderBy.php @@ -0,0 +1,19 @@ +asc !== 1 && $object->asc !== 0) || !in_array($object->value, $values)) return false; + + return true; + } + } +} diff --git a/server/libs/validations/validPriorities.php b/server/libs/validations/validPriorities.php new file mode 100644 index 00000000..aa3ba658 --- /dev/null +++ b/server/libs/validations/validPriorities.php @@ -0,0 +1,19 @@ +isNull()) return false; + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/server/models/Log.php b/server/models/Log.php index a1d99687..d9a234bc 100755 --- a/server/models/Log.php +++ b/server/models/Log.php @@ -22,21 +22,36 @@ class Log extends DataStore { 'authorUser', 'authorStaff', 'to', - 'date' + 'date', + 'authorName' ]; } - public static function createLog($type,$to, $author = null) { + public static function createLog($type, $to, $author = null) { + $session = Session::getInstance(); + $authorName = ''; + + if($session->isTicketSession()) { + $ticketNumber = $session->getTicketNumber(); + $ticket = Ticket::getByTicketNumber($ticketNumber); + $authorName = $ticket->authorToArray()['name']; + } + if($author === null) { $author = Controller::getLoggedUser(); } + if(!$author->isNull()) { + $authorName = $author->name; + } + $log = new Log(); $log->setProperties(array( 'type' => $type, 'to' => $to, - 'date' => Date::getCurrentDate() + 'date' => Date::getCurrentDate(), + 'authorName' => $authorName )); if($author instanceof User) { @@ -55,8 +70,8 @@ class Log extends DataStore { 'type' => $this->type, 'to' => $this->to, 'author' => [ - 'name' => $author->name, - 'id' => $author->id, + 'name' => $this->authorName, + 'id' => ($author && !$author->isNull()) ? $author->id : null, 'staff' => $author instanceof Staff ], 'date' => $this->date diff --git a/server/models/MailTemplate.php b/server/models/MailTemplate.php index 9d21196d..4482167b 100755 --- a/server/models/MailTemplate.php +++ b/server/models/MailTemplate.php @@ -19,6 +19,7 @@ class MailTemplate extends DataStore { const USER_SIGNUP = 'USER_SIGNUP'; const USER_PASSWORD = 'USER_PASSWORD'; const PASSWORD_FORGOT = 'PASSWORD_FORGOT'; + const USER_INVITE = 'USER_INVITE'; const USER_SYSTEM_DISABLED = 'USER_SYSTEM_DISABLED'; const USER_SYSTEM_ENABLED = 'USER_SYSTEM_ENABLED'; const TICKET_CREATED = 'TICKET_CREATED'; @@ -32,6 +33,7 @@ class MailTemplate extends DataStore { 'USER_PASSWORD' => 'data/mail-templates/user-edit-password.html', 'USER_EMAIL' => 'data/mail-templates/user-edit-email.html', 'PASSWORD_FORGOT' => 'data/mail-templates/user-password-forgot.html', + 'USER_INVITE' => 'data/mail-templates/user-invite.html', 'USER_SYSTEM_DISABLED' => 'data/mail-templates/user-system-disabled.html', 'USER_SYSTEM_ENABLED' => 'data/mail-templates/user-system-enabled.html', 'TICKET_CREATED' => 'data/mail-templates/ticket-created.html', diff --git a/server/models/Ticketevent.php b/server/models/Ticketevent.php index d22b961d..a8ea9b96 100755 --- a/server/models/Ticketevent.php +++ b/server/models/Ticketevent.php @@ -83,15 +83,16 @@ class Ticketevent extends DataStore { public function toArray() { $user = ($this->authorStaff) ? $this->authorStaff : $this->authorUser; + $author = $this->ticket->authorToArray(); return [ 'type' => $this->type, 'ticketNumber' => $this->ticket->ticketNumber, 'author' => [ - 'name' => $user ? $user->name : null, + 'name' => $user ? $user->name : $author['name'], 'staff' => $user instanceOf Staff, 'id' => $user ? $user->id : null, - 'customfields' => $user->xownCustomfieldvalueList ? $user->xownCustomfieldvalueList->toArray() : [], + 'customfields' => ($user && $user->xownCustomfieldvalueList) ? $user->xownCustomfieldvalueList->toArray() : [], ], 'edited' => $this->editedContent ]; diff --git a/server/tests/__mocks__/ControllerMock.php b/server/tests/__mocks__/ControllerMock.php index 7a4e8149..fa24452a 100755 --- a/server/tests/__mocks__/ControllerMock.php +++ b/server/tests/__mocks__/ControllerMock.php @@ -1,13 +1,19 @@ \Mock::stub()->returns(1) + ]); + Controller::$requestReturnMock = null; + $_SERVER['REMOTE_ADDR'] = 'MOCK_REMOTE'; + $this->searchController = new SearchController(); + } + + public function testTagsFilter() { + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [0] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( tag_ticket.tag_id = 0) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [0,1,2] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( tag_ticket.tag_id = 0 or tag_ticket.tag_id = 1 or tag_ticket.tag_id = 2) GROUP BY ticket.id' + ); + } + + public function testClosedFilter() { + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'closed'=> null + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'closed'=> 1 + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ticket.closed = 1 GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'closed'=> '0' + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ticket.closed = 0 GROUP BY ticket.id' + ); + } + public function testAssignedFilter(){ + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'assigned'=> null + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'assigned'=> '0' + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ticket.owner_id IS NULL GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'assigned'=> 1 + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ticket.owner_id IS NOT NULL GROUP BY ticket.id' + ); + } + public function testUnreadStaffFilter() { + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'unreadStaff' => null + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'unreadStaff' => '0' + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ticket.unread_staff = 0 GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'unreadStaff' => 1 + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ticket.unread_staff = 1 GROUP BY ticket.id' + ); + } + + public function testPriorityFilter() { + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [1] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( tag_ticket.tag_id = 1) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [2,3] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( tag_ticket.tag_id = 2 or tag_ticket.tag_id = 3) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'tags' => [1,2,3] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( tag_ticket.tag_id = 1 or tag_ticket.tag_id = 2 or tag_ticket.tag_id = 3) GROUP BY ticket.id' + ); + } + + public function testdateRangeFilter() { + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'dateRange' => null + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'dateRange' => [1,2] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE (ticket.date >= 1 and ticket.date <= 2) GROUP BY ticket.id' + ); + } + + public function testDepartmentsFilter() { + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'departments' => null, + 'allowedDepartments' => [ + [ + 'id' => 2 + ], + [ + 'id' => 1 + ], + [ + 'id' => 3 + ] + ] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( ticket.department_id = 2 or ticket.department_id = 1 or ticket.department_id = 3) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'departments' => [1], + 'allowedDepartments' => [ + [ + 'id' => 2 + ], + [ + 'id' => 1 + ], + [ + 'id' => 3 + ] + ] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( ticket.department_id = 1) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'departments' => [1,2,3], + 'allowedDepartments' => [ + [ + 'id' => 2 + ], + [ + 'id' => 1 + ], + [ + 'id' => 3 + ] + ] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( ticket.department_id = 1 or ticket.department_id = 2 or ticket.department_id = 3) GROUP BY ticket.id' + ); + } + + public function testAuthorsFilter() { + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'authors' => null + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'authors' => [ + [ + 'id' => 1, + 'staff' => 1 + ], + [ + 'id' => 2, + 'staff' => 0 + ] + ] + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE ( ticket.author_staff_id = 1 or ticket.author_id = 2) GROUP BY ticket.id' + ); + } + + public function testQueryFilter() { + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'query' => null + ]), + 'FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id' + ); + + $this->assertEquals( + $this->searchController->getSQLQuery([ + 'query' => 'hello world' + ]), + "FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE (ticket.title LIKE :query or ticket.content LIKE :query or ticket.ticket_number LIKE :query or (ticketevent.type = 'COMMENT' and ticketevent.content LIKE :query) ) GROUP BY ticket.id" + + ); + } + public function testQueryWithOrder() { + $this->assertEquals( + $this->searchController->getSQLQueryWithOrder([ + 'page' => 1 + ]), + "SELECT ticket.id FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id ORDER BY ticket.closed asc, ticket.owner_id asc, ticket.unread_staff asc, ticket.priority desc, ticket.date desc LIMIT 10 OFFSET 0" + ); + + $this->assertEquals( + $this->searchController->getSQLQueryWithOrder([ + 'page' => 1, + 'query' => 'stark' + ]), + "SELECT ticket.id FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) WHERE (ticket.title LIKE :query or ticket.content LIKE :query or ticket.ticket_number LIKE :query or (ticketevent.type = 'COMMENT' and ticketevent.content LIKE :query) ) GROUP BY ticket.id ORDER BY CASE WHEN (ticket.ticket_number LIKE :query) THEN ticket.ticket_number END desc,CASE WHEN (ticket.title LIKE :query) THEN ticket.title END desc, CASE WHEN ( ticket.content LIKE :query) THEN ticket.content END desc, CASE WHEN (ticketevent.type = 'COMMENT' and ticketevent.content LIKE :query) THEN ticketevent.content END desc,ticket.closed asc, ticket.owner_id asc, ticket.unread_staff asc, ticket.priority desc, ticket.date desc LIMIT 10 OFFSET 0" + ); + + $this->assertEquals( + $this->searchController->getSQLQueryWithOrder([ + 'page' => 1, + 'orderBy' => ['value' => 'closed', 'asc' => 1] + ]), + "SELECT ticket.id FROM (ticket LEFT JOIN tag_ticket ON tag_ticket.ticket_id = ticket.id LEFT JOIN ticketevent ON ticketevent.ticket_id = ticket.id) GROUP BY ticket.id ORDER BY ticket.closed asc,ticket.closed asc, ticket.owner_id asc, ticket.unread_staff asc, ticket.priority desc, ticket.date desc LIMIT 10 OFFSET 0" + ); + } +} diff --git a/server/tests/controllers/user/loginTest.php b/server/tests/controllers/user/loginTest.php index 035aaa70..d9615ff3 100755 --- a/server/tests/controllers/user/loginTest.php +++ b/server/tests/controllers/user/loginTest.php @@ -35,9 +35,9 @@ class LoginControllerTest extends TestCase { public function testShouldCreateSessionAndRespondSuccessIfCredentialsAreValid() { Session::mockInstanceFunction('sessionExists', \Mock::stub()->returns(false)); + Controller::useValueReturn(); $this->loginController->handler(); - $this->assertTrue(!!Session::getInstance()->createSession->hasBeenCalledWithArgs('MOCK_ID', false)); $this->assertTrue(Response::get('respondSuccess')->hasBeenCalledWithArgs(array( 'userId' => 'MOCK_ID', diff --git a/tests/init.rb b/tests/init.rb index 773866a6..eb082cb6 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -33,7 +33,7 @@ require './ticket/change-department.rb' require './ticket/close.rb' require './ticket/re-open.rb' require './ticket/delete.rb' -require './staff/add.rb' +require './staff/invite.rb' require './staff/get.rb' require './staff/edit.rb' require './staff/delete.rb' @@ -71,4 +71,5 @@ require './ticket/add-tag.rb' require './ticket/delete-tag.rb' require './ticket/edit-comment.rb' require './system/disable-user-system.rb' +require './ticket/search.rb' # require './system/get-stats.rb' diff --git a/tests/scripts.rb b/tests/scripts.rb index c2020d7d..2e7a7715 100644 --- a/tests/scripts.rb +++ b/tests/scripts.rb @@ -16,25 +16,33 @@ class Scripts }) end - def self.createStaff(email, password, name, level='1') + def self.createStaff(email, password, name, level='1') # WARNING: NOT USED ANYWHERE departments = request('/system/get-settings', { csrf_userid: $csrf_userid, csrf_token: $csrf_token })['data']['departments'] departments = departments.collect { |x| x.id } - response = request('/staff/add', { + response = request('/staff/invite', { :name => name, :email => email, - :password => password, :level => level, :departments => departments.to_string }) + recoverpassword = $database.getRow('recoverpassword', email, 'email') + + response = request('/user/recover-password', { + email: email, + password: password, + token: recoverpassword['token'] + }) + if response['status'] === 'fail' raise response['message'] end end + def self.deleteStaff(staffId) response = request('/staff/delete', { staffId: staffId, @@ -107,6 +115,7 @@ class Scripts description: description }) end + def self.createTag(name, color) request('/ticket/create-tag', { csrf_userid: $csrf_userid, @@ -115,6 +124,7 @@ class Scripts color: color }) end + def self.assignTicket(ticketnumber) request('/staff/assign-ticket', { ticketNumber: ticketnumber, @@ -122,6 +132,7 @@ class Scripts csrf_token: $csrf_token }) end + def self.commentTicket(ticketnumber,content) request('/ticket/comment', { content: content, diff --git a/tests/staff/edit.rb b/tests/staff/edit.rb index de00c46a..3afc28a2 100644 --- a/tests/staff/edit.rb +++ b/tests/staff/edit.rb @@ -32,17 +32,24 @@ describe'/staff/edit' do end it 'should edit own data staff' do - request('/staff/add', { + request('/staff/invite', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, name: 'Arya Stark', - password: 'starkpassword', email: 'arya@opensupports.com', level: 1, profilePic: '', departments: '[1]' }) + recoverpassword = $database.getRow('recoverpassword', 'arya@opensupports.com', 'email') + + request('/user/recover-password', { + email: 'arya@opensupports.com', + password: 'starkpassword', + token: recoverpassword['token'] + }) + row = $database.getRow('staff', 'arya@opensupports.com', 'email') result = request('/staff/edit', { diff --git a/tests/staff/get-all.rb b/tests/staff/get-all.rb index dec7fdc5..f8571c71 100644 --- a/tests/staff/get-all.rb +++ b/tests/staff/get-all.rb @@ -10,14 +10,18 @@ describe'/staff/get-all' do (result['status']).should.equal('success') + result['data'][0]['departments'] = result['data'][0]['departments'].sort_by do |department| + department['id'].to_i + end + (result['data'][0]['name']).should.equal('Emilia Clarke') (result['data'][0]['email']).should.equal('staff@opensupports.com') (result['data'][0]['profilePic']).should.equal('') (result['data'][0]['level']).should.equal('3') - (result['data'][0]['departments'][0]['id']).should.equal('2') - (result['data'][0]['departments'][0]['name']).should.equal('useless private deapartment') - (result['data'][0]['departments'][1]['id']).should.equal('1') - (result['data'][0]['departments'][1]['name']).should.equal('Help and Support') + (result['data'][0]['departments'][0]['id']).should.equal('1') + (result['data'][0]['departments'][0]['name']).should.equal('Help and Support') + (result['data'][0]['departments'][1]['id']).should.equal('2') + (result['data'][0]['departments'][1]['name']).should.equal('useless private deapartment') (result['data'][0]['departments'][2]['id']).should.equal('3') (result['data'][0]['departments'][2]['name']).should.equal('Suggestions') (result['data'][0]['assignedTickets']).should.equal(10) diff --git a/tests/staff/add.rb b/tests/staff/invite.rb similarity index 77% rename from tests/staff/add.rb rename to tests/staff/invite.rb index 92b3a92c..fd928e68 100644 --- a/tests/staff/add.rb +++ b/tests/staff/invite.rb @@ -1,21 +1,28 @@ -describe'/staff/add' do +describe'/staff/invite' do request('/user/logout') Scripts.login($staff[:email], $staff[:password], true) it 'should add staff member' do - result= request('/staff/add', { + + result = request('/staff/invite', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, name: 'Tyrion Lannister', email: 'tyrion@opensupports.com', - password: 'testpassword', level: 2, profilePic: '', departments: '[1]' }) - (result['status']).should.equal('success') + recoverpassword = $database.getRow('recoverpassword', 'tyrion@opensupports.com', 'email') + + request('/user/recover-password', { + email: 'tyrion@opensupports.com', + password: 'testpassword', + token: recoverpassword['token'] + }) + row = $database.getRow('staff', result['data']['id'], 'id') (row['name']).should.equal('Tyrion Lannister') @@ -27,16 +34,15 @@ describe'/staff/add' do (row['owners']).should.equal('4') lastLog = $database.getLastRow('log') - (lastLog['type']).should.equal('ADD_STAFF') + (lastLog['type']).should.equal('INVITE') end it 'should fail if staff member is alrady a staff' do - result= request('/staff/add', { + result = request('/staff/invite', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, name: 'Tyrion Lannister', email: 'tyrion@opensupports.com', - password: 'testpassword', level: 2, profilePic: '', departments: '[1]' diff --git a/tests/system/disable-user-system.rb b/tests/system/disable-user-system.rb index fa2185bb..05a2e10d 100644 --- a/tests/system/disable-user-system.rb +++ b/tests/system/disable-user-system.rb @@ -65,6 +65,35 @@ describe'system/disable-user-system' do (result['status']).should.equal('success') end + it 'should be able to comment on ticket as a non-logged user' do + result = request('/ticket/create', { + title: 'Doubt about Russian language', + content: 'Stariy means old in Russian?', + departmentId: 1, + language: 'en', + name: 'Abraham Einstein', + email: 'abrahameinstein@opensupports.com' + }) + (result['status']).should.equal('success') + + ticketNumber = result['data']['ticketNumber'] + + result = request('/ticket/check', { + ticketNumber: ticketNumber, + email: 'abrahameinstein@opensupports.com', + captcha: 'valid' + }) + token = result['data']['token'] + (result['status']).should.equal('success'); + + result = request('/ticket/comment', { + content: 'I actually think it is not like that, but anyways, thanks', + ticketNumber: ticketNumber, + csrf_token: token + }) + (result['status']).should.equal('success') + end + it 'should be able to assign and respond tickets' do Scripts.login($staff[:email], $staff[:password], true); ticket = $database.getLastRow('ticket'); @@ -84,6 +113,26 @@ describe'system/disable-user-system' do (result['status']).should.equal('success') end + it 'should be able to get the latest events as admin' do + result = request('/staff/last-events', { + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + (result['data'].size).should.equal(10) + end + + it 'should be able to get system logs as admin' do + result = request('/system/get-logs', { + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + (result['data'].size).should.equal(10) + end + it 'should be be able to create a ticket as an admin' do result = request('/ticket/create', { title: 'created by staff with user system disabled', @@ -128,7 +177,36 @@ describe'system/disable-user-system' do (result['message']).should.equal('SYSTEM_USER_IS_ALREADY_DISABLED') end + it 'should allow staff members to recover their passwords' do + request('/user/logout') + result = request('/user/send-recover-password', { + email: 'jorah@opensupports.com', + staff: true + }) + (result['status']).should.equal('success') + + token = $database.getLastRow('recoverpassword')['token']; + + result = request('/user/recover-password', { + email: 'jorah@opensupports.com', + password: 's3cur3p455w0rd', + token: token + }) + (result['status']).should.equal('success') + (result['data']['staff']).should.equal('1') + + result = request('/user/login', { + email: 'jorah@opensupports.com', + password: 's3cur3p455w0rd', + staff: true + }) + (result['status']).should.equal('success') + (result['data']['userEmail']).should.equal('jorah@opensupports.com') + end + it 'should enable the user system' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) result = request('/system/enable-user-system', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, @@ -143,7 +221,6 @@ describe'system/disable-user-system' do numberOftickets= $database.query("SELECT * FROM ticket WHERE author_email IS NULL AND author_name IS NULL AND author_id IS NOT NULL" ) (numberOftickets.num_rows).should.equal(53) - end it 'should not enable the user system' do @@ -155,6 +232,5 @@ describe'system/disable-user-system' do (result['status']).should.equal('fail') (result['message']).should.equal('SYSTEM_USER_IS_ALREADY_ENABLED') - end end diff --git a/tests/system/get-settings.rb b/tests/system/get-settings.rb index 383d1207..84ac35bd 100644 --- a/tests/system/get-settings.rb +++ b/tests/system/get-settings.rb @@ -1,7 +1,7 @@ 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]['name']).should.equal('Help and Support') diff --git a/tests/ticket/comment.rb b/tests/ticket/comment.rb index 8a0e1ab4..c5de7fc6 100644 --- a/tests/ticket/comment.rb +++ b/tests/ticket/comment.rb @@ -182,18 +182,28 @@ describe '/ticket/comment/' do request('/user/logout') Scripts.login($staff[:email], $staff[:password], true) - request('/staff/add', { + + result = request('/staff/invite', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, name: 'Jorah mormont', email: 'jorah@opensupports.com', - password: 'testpassword', level: 2, profilePic: '', departments: '[1]' }) + (result['status'].should.equal('success')) + request('/user/logout') + + recoverpassword = $database.getRow('recoverpassword', 'jorah@opensupports.com', 'email') + request('/user/recover-password', { + email: 'jorah@opensupports.com', + password: 'testpassword', + token: recoverpassword['token'] + }) + Scripts.login('jorah@opensupports.com', 'testpassword', true) result = request('/ticket/comment', { content: 'some comment content', diff --git a/tests/ticket/delete.rb b/tests/ticket/delete.rb index 6b0c5acf..c2add08c 100644 --- a/tests/ticket/delete.rb +++ b/tests/ticket/delete.rb @@ -6,17 +6,24 @@ describe '/ticket/delete' do Scripts.createTicket('ticket_to_delete') ticket = $database.getRow('ticket', 'ticket_to_delete', 'title') - request('/staff/add', { + request('/staff/invite', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, name: 'Ned Stark', - password: 'headless', email: 'ned@opensupports.com', level: 3, profilePic: '', departments: '[1]' }) + recoverpassword = $database.getRow('recoverpassword', 'ned@opensupports.com', 'email') + + request('/user/recover-password', { + email: 'ned@opensupports.com', + password: 'headless', + token: recoverpassword['token'] + }) + request('/user/logout') Scripts.login('ned@opensupports.com', 'headless', true) @@ -80,16 +87,24 @@ describe '/ticket/delete' do ticket = $database.getRow('ticket', 'ticket_to_delete_4', 'title'); - request('/staff/add', { - csrf_userid: $csrf_userid, - csrf_token: $csrf_token, - name: 'Joan Chris', - password: 'theyaregonnafireme', - email: 'uselessstaff@opensupports.com', - level: 2, - profilePic: '', - departments: '[1]' + request('/staff/invite', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'Joan Chris', + email: 'uselessstaff@opensupports.com', + level: 2, + profilePic: '', + departments: '[1]' }) + + recoverpassword = $database.getRow('recoverpassword', 'uselessstaff@opensupports.com', 'email') + + request('/user/recover-password', { + email: 'uselessstaff@opensupports.com', + password: 'theyaregonnafireme', + token: recoverpassword['token'] + }) + request('/user/logout') Scripts.login('uselessstaff@opensupports.com', 'theyaregonnafireme',true) diff --git a/tests/ticket/search.rb b/tests/ticket/search.rb new file mode 100644 index 00000000..2ae320d6 --- /dev/null +++ b/tests/ticket/search.rb @@ -0,0 +1,145 @@ +describe '/ticket/search' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + + it 'should fail if the page is invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: -1 + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_PAGE') + end + + it 'should fail if the tags are invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + tags: "[1,11,111,1111,11111,111111,1111111,11111111]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_TAG_FILTER') + end + + it 'should fail if the closed value is invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + closed: 3 + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_CLOSED_FILTER') + end + + it 'should fail if the unreadStaff value is invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + unreadStaff: 3 + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_UNREAD_STAFF_FILTER') + end + + it 'should fail if the priority values are invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + priority: "[0,1,5,6]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_PRIORITY_FILTER') + end + + it 'should fail if the priority' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + priority: "[0,1,),hi]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_PRIORITY_FILTER') + end + + it 'should fail if the dateRange values are invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + dateRange: "[11,69,()) ]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_DATE_RANGE_FILTER') + end + + it 'should fail if the departments are invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + departments: "[-1,-2,99]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_DEPARTMENT_FILTER') + end + + it 'should fail if the authors are invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + authors: "[{id:30001, staff: 1},{id:30,staff: 3}]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_AUTHOR_FILTER') + + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + authors: "[{id:'delete all)', staff: 1},{id:30,staff: 3}]" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_AUTHOR_FILTER') + end + + it 'should fail if the assigned value is invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + assigned: 3 + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_ASSIGNED_FILTER') + end + + it 'should fail if the assigned value is invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + assigned: 11113 + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_ASSIGNED_FILTER') + end + + it 'should fail if the orderBy values are invalid' do + result = request('/ticket/search', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + page: 1, + orderBy: "{value: 'closeddd', asc: 11}" + }) + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_ORDER_BY') + end +end