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/admin/panel/admin-panel-menu.js b/client/src/app/admin/panel/admin-panel-menu.js index 20734df3..9627947a 100644 --- a/client/src/app/admin/panel/admin-panel-menu.js +++ b/client/src/app/admin/panel/admin-panel-menu.js @@ -107,18 +107,18 @@ class AdminPanelMenu extends React.Component { getRoutes() { const customLists = this.getCustomlists(); - return this.getItemsByFilteredByLevel([ + 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', @@ -155,7 +155,7 @@ class AdminPanelMenu extends React.Component { ...customLists ]) }, - { + this.props.config['user-system-enabled'] ? { groupName: i18n('USERS'), path: '/admin/panel/users', icon: 'user', @@ -177,7 +177,7 @@ class AdminPanelMenu extends React.Component { level: 1 } ]) - }, + } : null, { groupName: i18n('ARTICLES'), path: '/admin/panel/articles', @@ -192,7 +192,6 @@ class AdminPanelMenu extends React.Component { ]) }, { - groupName: i18n('STAFF'), path: '/admin/panel/staff', icon: 'users', @@ -239,7 +238,7 @@ class AdminPanelMenu extends React.Component { } ]) } - ]); + ], null)); } getItemsByFilteredByLevel(items) { @@ -249,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/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/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..b0a77964 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 }; @@ -153,6 +152,8 @@ class MainSignUpWidget extends React.Component { this.setState({ loading: false, message: 'success' + }, () => { + setTimeout(() => {history.push('/check-ticket')}, 2000); }); } 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 b29c60d1..f244d610 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', @@ -194,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', @@ -233,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', @@ -338,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', @@ -374,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/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.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/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 432b6d08..1ea18790 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/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 8ae934ab..ec6736cb 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/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 + + + + + + + + + + + +
+ + + + + + + + + +
+
+ + + + +
+
+ + + + +
+ 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/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/tests/init.rb b/tests/init.rb index 5f3ef8a0..ffffdcb7 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' diff --git a/tests/scripts.rb b/tests/scripts.rb index 7c1e7bf0..e0c7e24c 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, @@ -106,6 +114,7 @@ class Scripts description: description }) end + def self.createTag(name, color) request('/ticket/create-tag', { csrf_userid: $csrf_userid, @@ -114,6 +123,7 @@ class Scripts color: color }) end + def self.assignTicket(ticketnumber) request('/staff/assign-ticket', { ticketNumber: ticketnumber, @@ -121,6 +131,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/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 11a6c33d..9ae8dd15 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', @@ -113,7 +162,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, @@ -127,8 +205,7 @@ 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(52) - + (numberOftickets.num_rows).should.equal(53) end it 'should not enable the user system' do @@ -140,6 +217,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/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)