diff --git a/client/package.json b/client/package.json index d4d13866..e26d156b 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "OpenSupports", - "version": "4.2.0", + "version": "4.7.0", "author": "Ivan Diaz ", "description": "Open source ticket system made with PHP and ReactJS", "repository": { diff --git a/client/src/app-components/ticket-query-filters.js b/client/src/app-components/ticket-query-filters.js index 89cad7b0..f59919c4 100644 --- a/client/src/app-components/ticket-query-filters.js +++ b/client/src/app-components/ticket-query-filters.js @@ -92,7 +92,7 @@ class TicketQueryFilters extends React.Component { }} />
- Authors + {i18n('AUTHORS')}
+
+
{i18n('SUPERVISED_USER')}
+
+ + +
+ {this.renderSupervisedUserMessage()} +
+
{i18n('TICKETS')}
@@ -87,6 +110,92 @@ class AdminPanelViewUser extends React.Component {
); } + renderSupervisedUserMessage(){ + if(this.state.message) { + if(this.state.message != 'success'){ + return( +
+ {i18n(this.state.message)} +
+ ) + }else{ + return( +
+ {i18n('SUPERVISED_USERS_UPDATED')} +
+ ) + } + } + } + + onClickSupervisorUserButton(){ + this.setState({ + loading: true + }); + + const userIdList = this.state.userList.map((item) => { + return item.id; + }); + + API.call({ + path: '/user/edit-supervised-list', + data: { + userIdList: JSON.stringify(userIdList), + userId: this.props.params.userId + } + }).then(r => { + this.setState({ + loading: false, + message: 'success' + }) + }).catch((r) => { + this.setState({ + loading: false, + message: r.message + }) + }); + } + + onChangeValues(newList) { + this.setState({ + userList: newList + }); + } + searchUsers(query, blacklist = []) { + return API.call({ + path: '/ticket/search-authors', + data: { + query: query, + blackList: JSON.stringify(blacklist), + searchUsers: 1 + } + }).then(r => { + return r.data.authors.map(author => { + return { + name: author.name, + color: "gray", + id: author.id*1, + content:
{author.name}
, + isStaff: false + }}); + }).catch((r) => { + console.log(r); + }); + } + + transformUserListToAutocomplete() { + return( + this.state.userList.map((user) => { + return ({ + id: user.id*1, + name: user.name, + content:
{user.name}
, + color: 'grey', + isStaff: false + }); + }) + ); + } renderNotVerified() { return ( @@ -129,7 +238,8 @@ class AdminPanelViewUser extends React.Component { tickets: result.data.tickets, disabled: result.data.disabled, customfields: result.data.customfields, - loading: false + loading: false, + userList: result.data.userList }); } diff --git a/client/src/app/admin/panel/users/admin-panel-view-user.scss b/client/src/app/admin/panel/users/admin-panel-view-user.scss index b7cd144b..b35397fa 100644 --- a/client/src/app/admin/panel/users/admin-panel-view-user.scss +++ b/client/src/app/admin/panel/users/admin-panel-view-user.scss @@ -34,7 +34,28 @@ text-align: left; } + &__supervised-users-header { + font-size: $font-size--md; + margin-bottom: 20px; + text-align: left; + } + + &__supervised-users-content { + display:flex; + flex-direction: column; + align-items:flex-start; + } + + &__supervised-users-message { + margin-top: 20px + } + + &__submit-button { + margin-top: 20px; + } + &__unverified { margin-left: 15px; } + } diff --git a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js index 352dbe40..5f863a7e 100644 --- a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js +++ b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.js @@ -4,8 +4,13 @@ import {connect} from 'react-redux'; import SessionActions from 'actions/session-actions'; import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; import Header from 'core-components/header'; +import Form from 'core-components/form'; +import FormField from 'core-components/form-field'; +import Message from 'core-components/message'; + import TicketList from 'app-components/ticket-list'; class DashboardListTicketsPage extends React.Component { @@ -16,28 +21,101 @@ class DashboardListTicketsPage extends React.Component { static defaultProps = { tickets: [] }; + + state = { + tickets: [], + pages: 1, + page: 1, + form:{ + users: [], + ownTickets: true + }, + message: '' + }; componentDidMount() { this.retrieveUserData(); + this.updateTicketList({users: [], ownTickets: true}); } render() { return (
- + {this.props.userUsers.length ? this.showSupervisorOptions() : null} + + {this.state.message ? {i18n(this.state.message)} : null}
); } - retrieveUserData() { this.props.dispatch(SessionActions.getUserData()); } + onPageChange(e){ + this.updateTicketList({...this.state.form, page: e.target.value}); + } + showSupervisorOptions(){ + return( +
+ + + + ) + } + + getUserItems() { + let usersList = this.props.userUsers.map(user => { + return { + id: user.id*1, + name: user.name, + content:
{user.name}
, + color: 'grey', + isStaff: false + }; + }); + return usersList; + } + + onFormChange(form){ + this.updateTicketList(form); + this.setState({ + form + }); + } + + updateTicketList({users, page, ownTickets}) { + let usersIds = users.map((item) => { + return item.id + }) + + API.call({ + path: 'user/get-supervised-tickets', + data: { + page, + showOwnTickets: ownTickets*1, + supervisedUsers: JSON.stringify(usersIds) + } + }).then((r) => { + this.setState({ + tickets: r.data.tickets, + pages: r.data.pages, + page: r.data.page, + message: '' + }) + }).catch((r) => { + this.setState({ + tickets: [], + message: r.message + }) + }); + + } } export default connect((store) => { return { - tickets: store.session.userTickets + userId: store.session.userId, + userUsers: store.session.userUsers || [] }; })(DashboardListTicketsPage); diff --git a/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.scss b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.scss new file mode 100644 index 00000000..098f45e3 --- /dev/null +++ b/client/src/app/main/dashboard/dashboard-list-tickets/dashboard-list-tickets-page.scss @@ -0,0 +1,12 @@ +.dashboard-ticket-list { + + &__supervisor-form { + display: flex; + align-items: center; + justify-content: flex-start; + + &-checkbox { + margin-left: 20px; + } + } +} \ No newline at end of file diff --git a/client/src/core-components/autocomplete.js b/client/src/core-components/autocomplete.js index 61781ad5..6c079aff 100644 --- a/client/src/core-components/autocomplete.js +++ b/client/src/core-components/autocomplete.js @@ -159,7 +159,6 @@ class Autocomplete extends React.Component { if(this.getDropdownList().length) { const selectedItem = this.getDropdownList()[e.index]; const newList = [...this.getSelectedItems(), selectedItem]; - this.setState({ selectedItems: newList, inputValue: "", diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 5126a422..32c46237 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -26,9 +26,11 @@ export default { 'CREATE_TICKET': 'Create Ticket', 'TICKET_LIST': 'Ticket List', 'SUPPORT_CENTER': 'Support Center', + 'SUPERVISED_USER': 'Supervised users', 'DEPARTMENT': 'Department', 'DEFAULT_DEPARTMENT': 'Default Department', 'AUTHOR': 'Author', + 'AUTHORS': 'Authors', 'DATE': 'Date', 'RESPOND': 'Respond', 'RESPOND_TICKET': 'Respond Ticket', @@ -198,6 +200,7 @@ export default { 'SUBJECT': 'Subject', 'SEND_EMAIL_ON_NEW_TICKET': 'Send email on new ticket', 'STAFF_UPDATED': 'Staff member has been updated', + 'SUPERVISED_USERS_UPDATED': 'Supervised users updated', 'UPDATE': 'Update', 'NEVER': 'Never', 'HIMSELF': 'himself', @@ -210,6 +213,7 @@ export default { 'ENABLE_USER': 'Enable User', 'DISABLE_USER': 'Disable User', 'SHOW_CLOSED_TICKETS': 'Show Closed Tickets', + 'SHOW_MY_TICKETS': 'Show my tickets', 'IMAGE_HEADER_URL': 'Image header URL', 'IMAGE_HEADER_DESCRIPTION': 'Image that will be used as header of the email', 'EMAIL_SETTINGS': 'Email Settings', @@ -390,6 +394,8 @@ export default { 'CURRENTLY_UNAVAILABLE': 'Currently unavailable', 'CAN_NOT_DELETE_DEFAULT_DEPARTMENT': 'Default department can not be deleted', 'INVALID_DEFAULT_DEPARTMENT': 'Default department choosen is invalid', + 'INVALID_SUPERVISED_USERS': 'Invalid supervised users', + 'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF': 'Supervisor can not supervise himself', //MESSAGES 'SIGNUP_SUCCESS': 'You have registered successfully in our support system.', diff --git a/client/src/reducers/session-reducer.js b/client/src/reducers/session-reducer.js index 08374ad1..a329cebc 100644 --- a/client/src/reducers/session-reducer.js +++ b/client/src/reducers/session-reducer.js @@ -106,7 +106,6 @@ class SessionReducer extends Reducer { onUserDataRetrieved(state, payload) { let userData = payload.data; sessionStore.storeUserData(payload.data); - return _.extend({}, state, { staff: userData.staff, userName: userData.name, @@ -116,7 +115,8 @@ class SessionReducer extends Reducer { userDepartments: userData.departments, userTickets: userData.tickets, userSendEmailOnNewTicket: userData.sendEmailOnNewTicket * 1, - userCustomFields: userData.customfields + userCustomFields: userData.customfields, + userUsers: userData.users }); } diff --git a/server/controllers/system/delete-department.php b/server/controllers/system/delete-department.php index e8931f74..e8366ac5 100755 --- a/server/controllers/system/delete-department.php +++ b/server/controllers/system/delete-department.php @@ -76,7 +76,7 @@ class DeleteDepartmentController extends Controller { public function transferDepartmentTickets() { $tickets = Ticket::find('department_id = ?', [$this->departmentId]); - $newDepartment = Department::getDataStore($this->transferDepartmentId);; + $newDepartment = Department::getDataStore($this->transferDepartmentId); foreach($tickets as $ticket) { $staffOwner = $ticket->owner; diff --git a/server/controllers/ticket/search-authors.php b/server/controllers/ticket/search-authors.php index 05e12a48..c3128b33 100644 --- a/server/controllers/ticket/search-authors.php +++ b/server/controllers/ticket/search-authors.php @@ -5,7 +5,7 @@ DataValidator::with('CustomValidations', true); /** * @api {post} /ticket/search-authors search authors of tickets - * @apiVersion 4.6.1 + * @apiVersion 4.7 * * @apiName Search authors * @@ -41,6 +41,10 @@ class SearchAuthorsController extends Controller { 'blackList' => [ 'validation' => DataValidator::oneOf(DataValidator::validAuthorsBlackList(),DataValidator::nullType()), 'error' => ERRORS::INVALID_BLACK_LIST + ], + 'searchUsers' => [ + 'validation' => DataValidator::oneOf(DataValidator::in(['0','1']),DataValidator::nullType()), + 'error' => ERRORS::INVALID_USER_SEARCH_OPTION ] ] ]; @@ -48,31 +52,44 @@ class SearchAuthorsController extends Controller { public function handler() { $query = Controller::request('query'); + $searchUser = Controller::request('searchUsers') ? Controller::request('searchUsers') : 0; - $authorsQuery = "SELECT id,name,level FROM staff " . $this->generateAuthorsIdQuery($query) . " LIMIT 10"; - $authorsMatch = RedBean::getAll($authorsQuery, [':query' => "%" .$query . "%",':queryAtBeginning' => $query . "%"] ); - $authors = []; + if(!$searchUser){ + $sqlQuery = $this->generateAuthorsIdQuery($query); + }else{ + $sqlQuery = $this->generateUsersIdQuery($query); + } - foreach($authorsMatch as $authorMatch) { - if($authorMatch['level'] >=1 && $authorMatch['level'] <= 3){ - $author = Staff::getDataStore($authorMatch['id']*1); + $dataStoresMatch = RedBean::getAll($sqlQuery, [':query' => "%" .$query . "%",':queryAtBeginning' => $query . "%"] ); + + $list = []; + foreach($dataStoresMatch as $dataStoreMatch) { + if(!$searchUser && $dataStoreMatch['level'] >=1 && $dataStoreMatch['level'] <= 3){ + $dataStore = Staff::getDataStore($dataStoreMatch['id']*1); } else { - $author = User::getDataStore($authorMatch['id']*1); + $dataStore = User::getDataStore($dataStoreMatch['id']*1); } - array_push($authors, $author->toArray(true)); + array_push($list, $dataStore->toArray(true)); } Response::respondSuccess([ - 'authors' => $authors + 'authors' => $list ]); } public function generateAuthorsIdQuery($query) { if ($query){ - return "WHERE name LIKE :query " . $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE name LIKE :query " . $this->generateUserBlackListQuery() . " ORDER BY CASE WHEN (name LIKE :queryAtBeginning) THEN 1 ELSE 2 END ASC "; + return "SELECT id,name, level FROM staff WHERE name LIKE :query " . $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE name LIKE :query " . $this->generateUserBlackListQuery() . " ORDER BY CASE WHEN (name LIKE :queryAtBeginning) THEN 1 ELSE 2 END ASC LIMIT 10"; } else { - return "WHERE 1=1 ". $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE 1=1". $this->generateUserBlackListQuery() ." ORDER BY id"; + return "SELECT id,name, level FROM staff WHERE 1=1 ". $this->generateStaffBlackListQuery() . " UNION SELECT id,name,signup_date FROM user WHERE 1=1". $this->generateUserBlackListQuery() ." ORDER BY id LIMIT 10"; } } - + public function generateUsersIdQuery($query) { + if ($query){ + return "SELECT id FROM user WHERE name LIKE :query " . $this->generateUserBlackListQuery() . " ORDER BY CASE WHEN (name LIKE :queryAtBeginning) THEN 1 ELSE 2 END ASC LIMIT 10"; + } else { + return "SELECT id FROM user WHERE 1=1 ". $this->generateUserBlackListQuery() ." ORDER BY id LIMIT 10"; + } + } + public function generateStaffBlackListQuery(){ $StaffBlackList = $this->getBlackListFiltered(); return $this->generateBlackListQuery($StaffBlackList); diff --git a/server/controllers/ticket/search.php b/server/controllers/ticket/search.php index 8700cb44..4a91838c 100644 --- a/server/controllers/ticket/search.php +++ b/server/controllers/ticket/search.php @@ -27,6 +27,7 @@ DataValidator::with('CustomValidations', true); * @apiParam {Number} page The number of the page of the tickets. * @apiParam {Object} orderBy A object {value, asc}with string and boolean to make a especific order of the search. * @apiParam {Number[]} owners The ids of the owners to make a custom search. + * @apiParam {Boolean} supervisor Boolean to deteminate if a super-user is making the call. * * @apiUse NO_PERMISSION * @apiUse INVALID_TAG_FILTER @@ -48,6 +49,7 @@ DataValidator::with('CustomValidations', true); class SearchController extends Controller { const PATH = '/search'; const METHOD = 'POST'; + private $ignoreDeparmentFilter; public function validations() { return [ @@ -102,6 +104,7 @@ class SearchController extends Controller { } public function handler() { + $this->ignoreDeparmentFilter = (bool)Controller::request('supervisor'); $allowedDepartmentsId = []; foreach (Controller::getLoggedUser()->sharedDepartmentList->toArray() as $department) { @@ -124,7 +127,6 @@ class SearchController extends Controller { 'staffId' => Controller::getLoggedUser()->id ]; - $query = $this->getSQLQuery($inputs); $queryWithOrder = $this->getSQLQueryWithOrder($inputs); $totalCount = RedBean::getAll("SELECT COUNT(*) FROM (SELECT COUNT(*) " . $query . " ) AS T2", [':query' => "%" . $inputs['query'] . "%", ':queryAtBeginning' => $inputs['query'] . "%" ])[0]['COUNT(*)']; @@ -180,7 +182,7 @@ class SearchController extends Controller { if(array_key_exists('unreadStaff',$inputs)) $this->setSeenFilter($inputs['unreadStaff'], $filters); if(array_key_exists('dateRange',$inputs)) $this->setDateFilter($inputs['dateRange'], $filters); if(array_key_exists('departments',$inputs) && array_key_exists('allowedDepartments',$inputs) && array_key_exists('staffId',$inputs)){ - $this->setDepartmentFilter($inputs['departments'],$inputs['allowedDepartments'], $inputs['staffId'], $filters); + if(!$this->ignoreDeparmentFilter) $this->setDepartmentFilter($inputs['departments'],$inputs['allowedDepartments'], $inputs['staffId'], $filters); } if(array_key_exists('authors',$inputs)) $this->setAuthorFilter($inputs['authors'], $filters); if(array_key_exists('owners',$inputs)) $this->setOwnerFilter($inputs['owners'], $filters); diff --git a/server/controllers/user.php b/server/controllers/user.php index feb48483..3c8e1c1e 100755 --- a/server/controllers/user.php +++ b/server/controllers/user.php @@ -22,5 +22,7 @@ $userControllers->addController(new VerifyController); $userControllers->addController(new EnableUserController); $userControllers->addController(new DisableUserController); $userControllers->addController(new EditCustomFieldsController); +$userControllers->addController(new EditSupervisedListController); +$userControllers->addController(new GetSupervisedTicketController); $userControllers->finalize(); diff --git a/server/controllers/user/edit-supervised-list.php b/server/controllers/user/edit-supervised-list.php new file mode 100644 index 00000000..88eecc78 --- /dev/null +++ b/server/controllers/user/edit-supervised-list.php @@ -0,0 +1,81 @@ + 'staff_1', + 'requestData' => [ + 'userIdList' => [ + 'validation' => DataValidator::validUsersId(), + 'error' => ERRORS::INVALID_LIST + ], + 'userId' => [ + 'validation' => DataValidator::dataStoreId('user'), + 'error' => ERRORS::INVALID_USER + ] + ] + ]; + } + + public function handler() { + $userIdList = $this->getUserIdListCleared(); + $superUser = User::getDataStore(Controller::request('userId')); + + if(!$superUser->supervisedrelation) { + $superUser->supervisedrelation = new Supervisedrelation(); + } + + $superUser->supervisedrelation->sharedUserList->clear(); + foreach($userIdList as $userId) { + $user = User::getDataStore($userId); + + $superUser->supervisedrelation->sharedUserList->add($user); + + } + + $superUser->supervisedrelation->store(); + $superUser->store(); + + Response::respondSuccess(); + } + + public function getUserIdListCleared(){ + $clearedList = array_unique(json_decode(Controller::request('userIdList'))); + $superUser = User::getDataStore(Controller::request('userId')); + + foreach ($clearedList as $item) { + if($item == $superUser->id) throw new Exception(ERRORS::SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF); + } + + return $clearedList; + } + +} \ No newline at end of file diff --git a/server/controllers/user/get-supervised-tickets.php b/server/controllers/user/get-supervised-tickets.php new file mode 100644 index 00000000..3a67018e --- /dev/null +++ b/server/controllers/user/get-supervised-tickets.php @@ -0,0 +1,115 @@ + 'user', + 'requestData' => [ + 'supervisedUsers' => [ + 'validation' => DataValidator::oneOf(DataValidator::validUsersId(),DataValidator::nullType()), + 'error' => ERRORS:: INVALID_SUPERVISED_USERS + ], + 'page' => [ + 'validation' => DataValidator::oneOf(DataValidator::numeric()->positive(),DataValidator::nullType()), + 'error' => ERRORS::INVALID_PAGE + ] + ] + ]; + } + private $authors; + private $page; + private $showOwnTickets; + private $supervisedUserList; + + public function handler() { + $this->page = Controller::request('page') ? Controller::request('page') : 1; + $this->showOwnTickets = (bool)Controller::request('showOwnTickets'); + $this->supervisedUserList = Controller::request('supervisedUsers')? json_decode(Controller::request('supervisedUsers')) : []; + $this->authors = $this->createAuthorsArray(); + + if(!$this->canUserHandleSupervisedUsers()){ + throw new Exception(ERRORS::INVALID_SUPERVISED_USERS); + } + + $searchController = new SearchController(true); + Controller::setDataRequester(function ($key) { + switch ($key) { + case 'authors': + return json_encode($this->authors); + case 'page' : + return $this->page*1; + case 'supervisor': + return 1; + } + + return null; + }); + + if(empty($this->authors)) { + Response::respondSuccess([]); + }else{ + $searchController->handler(); + } + } + + public function canUserHandleSupervisedUsers() { + $user = Controller::getLoggedUser(); + if(!$user->supervisedrelation && $this->supervisedUserList) return false; + + if(!empty($this->supervisedUserList)){ + foreach($this->supervisedUserList as $supevisedUser) { + if(!$user->supervisedrelation->sharedUserList->includesId($supevisedUser) && $supevisedUser != $user->id){ + return false; + } + } + } + return true; + } + + public function createAuthorsArray(){ + $user = Controller::getLoggedUser(); + + $authors = []; + + if(!empty($this->supervisedUserList)){ + foreach(array_unique($this->supervisedUserList) as $supervised){ + array_push($authors,['id'=> $supervised,'isStaff'=> 0]); + } + }; + + if(!in_array( $user->id, $this->supervisedUserList) && $this->showOwnTickets){ + array_push($authors,['id'=> $user->id*1,'isStaff'=> 0]); + } + return $authors; + } +} \ No newline at end of file diff --git a/server/controllers/user/get-user.php b/server/controllers/user/get-user.php index a5e683af..d534f061 100755 --- a/server/controllers/user/get-user.php +++ b/server/controllers/user/get-user.php @@ -57,7 +57,6 @@ class GetUserByIdController extends Controller { $tickets->add($ticket); } } - Response::respondSuccess([ 'name' => $user->name, 'email' => $user->email, @@ -66,6 +65,7 @@ class GetUserByIdController extends Controller { 'verified' => !$user->verificationToken, 'disabled' => !!$user->disabled, 'customfields' => $user->xownCustomfieldvalueList->toArray(), + 'userList' => $user->supervisedrelation ? $user->supervisedrelation->sharedUserList->toArray() : [] ]); } } diff --git a/server/controllers/user/get.php b/server/controllers/user/get.php index 5b699ffc..7bf5e50b 100755 --- a/server/controllers/user/get.php +++ b/server/controllers/user/get.php @@ -38,6 +38,7 @@ class GetUserController extends Controller { } public function handler() { + if (Controller::isStaffLogged()) { throw new RequestException(ERRORS::INVALID_CREDENTIALS); return; @@ -50,7 +51,7 @@ class GetUserController extends Controller { foreach($ticketList as $ticket) { $parsedTicketList[] = $ticket->toArray(true); } - + Response::respondSuccess([ 'name' => $user->name, 'email' => $user->email, @@ -58,6 +59,7 @@ class GetUserController extends Controller { 'verified' => !$user->verificationToken, 'tickets' => $parsedTicketList, 'customfields' => $user->xownCustomfieldvalueList->toArray(), + 'users' => $user->supervisedrelation ? $user->supervisedrelation->sharedUserList->toArray() : null ]); } } diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index e304c537..c5f30ee7 100755 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -267,6 +267,10 @@ * @apiDefine DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE * @apiError {String} DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE Default Department can not be private */ +/** + * @apiDefine SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF + * @apiError {String} SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF The supervisor cannot select himself + */ /** * @apiDefine EMAIL_POLLING * @apiError {String} EMAIL_POLLING Email polling was unsuccesful @@ -314,6 +318,10 @@ /** * @apiDefine REGISTRATION_IS_DESACTIVATED * @apiError {String} REGISTRATION_IS_DESACTIVATED Registration is disactivated + */ +/** + * @apiDefine INVALID_SUPERVISED_USERS + * @apiError {String} INVALID_SUPERVISED_USERS supervised users are invalid */ class ERRORS { @@ -386,6 +394,7 @@ class ERRORS { const INVALID_TEXT_3 = 'INVALID_TEXT_3'; const DEPARTMENT_PRIVATE_TICKETS = 'DEPARTMENT_PRIVATE_TICKETS'; const DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE = 'DEFAULT_DEPARTMENT_CAN_NOT_BE_PRIVATE'; + const SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF = 'SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF'; const EMAIL_POLLING = 'EMAIL_POLLING'; const CUSTOM_FIELD_ALREADY_EXISTS = 'CUSTOM_FIELD_ALREADY_EXISTS'; const INVALID_CUSTOM_FIELD = 'INVALID_CUSTOM_FIELD'; @@ -397,4 +406,6 @@ class ERRORS { const INVALID_API_KEY_TYPE = 'INVALID_API_KEY_TYPE'; const MANDATORY_LOGIN_IS_DESACTIVATED = 'MANDATORY_LOGIN_IS_DESACTIVATED'; const REGISTRATION_IS_DESACTIVATED = 'REGISTRATION_IS_DESACTIVATED'; + const INVALID_SUPERVISED_USERS = 'INVALID_SUPERVISED_USERS'; + const INVALID_USER_SEARCH_OPTION = 'INVALID_USER_SEARCH_OPTION'; } diff --git a/server/libs/DataStoreList.php b/server/libs/DataStoreList.php index 38d429fc..a60a5027 100755 --- a/server/libs/DataStoreList.php +++ b/server/libs/DataStoreList.php @@ -52,6 +52,12 @@ class DataStoreList implements IteratorAggregate { return empty($this->list); } + public function clear() { + foreach($this->list as $index=>$item) { + unset($this->list[$index]); + } + } + public function toBeanList() { $beanList = []; @@ -62,7 +68,7 @@ class DataStoreList implements IteratorAggregate { return $beanList; } - + public function toArray($minimized = false) { $array = []; diff --git a/server/libs/validations/validUsersId.php b/server/libs/validations/validUsersId.php new file mode 100644 index 00000000..bcdea41e --- /dev/null +++ b/server/libs/validations/validUsersId.php @@ -0,0 +1,18 @@ +isNull()) return false; + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/server/models/DataStore.php b/server/models/DataStore.php index 9e56df92..b14af3df 100755 --- a/server/models/DataStore.php +++ b/server/models/DataStore.php @@ -130,7 +130,7 @@ abstract class DataStore { $parsedProp = $this->_bean[$prop]; } - if (strpos($prop, 'List')) { + if (strpos($prop, 'List') || strpos($prop, 'shared') === 0) { $parsedProp = DataStoreList::getList($this->getListType($prop), $parsedProp); } else if ($parsedProp instanceof \RedBeanPHP\OODBBean) { $beanType = ucfirst($parsedProp->getPropertiesAndType()[1]); diff --git a/server/models/SupervisedRelation.php b/server/models/SupervisedRelation.php new file mode 100644 index 00000000..ab479dd8 --- /dev/null +++ b/server/models/SupervisedRelation.php @@ -0,0 +1,20 @@ + $this->sharedUserList + ]; + } +} \ No newline at end of file diff --git a/server/models/User.php b/server/models/User.php index 79771e01..593d4841 100755 --- a/server/models/User.php +++ b/server/models/User.php @@ -33,7 +33,9 @@ class User extends DataStore { 'verificationToken', 'disabled', 'xownCustomfieldvalueList', - 'notRegistered' + 'notRegistered', + 'sharedUserList', + 'supervisedrelation' ]; } @@ -47,12 +49,17 @@ class User extends DataStore { public function canManageTicket(Ticket $ticket){ $ticketNumberInstanceValidation = true; - + $ticketOfSupervisedUser = false; + if($this->ticketNumber) { $ticketNumberInstanceValidation = $this->ticketNumber == $ticket->ticketNumber; } - - return ($ticket->isAuthor($this) && $ticketNumberInstanceValidation); + if($this->supervisedrelation){ + foreach( $this->supervisedrelation->sharedUserList as $user){ + if($ticket->isAuthor($user)) $ticketOfSupervisedUser = true; + } + } + return (($ticket->isAuthor($this) || $ticketOfSupervisedUser) && $ticketNumberInstanceValidation); } public function toArray($minimal = false) { @@ -72,7 +79,8 @@ class User extends DataStore { 'verified' => !$this->verificationToken, 'disabled' => $this->disabled, 'customfields' => $this->xownCustomfieldvalueList->toArray(), - 'notRegistered' => $this->notRegistered + 'notRegistered' => $this->notRegistered, + 'supervisedrelation' => $this->supervisedrelation ]; } } diff --git a/tests/init.rb b/tests/init.rb index 91f3223b..b9a6846e 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -77,4 +77,6 @@ require './ticket/search.rb' require './ticket/get-authors.rb' require './system/mandatory-login.rb' require './system/default-department.rb' +require './user/edit-supervised-list.rb' +require './user/get-supervised-tickets.rb' # require './system/get-stats.rb' diff --git a/tests/user/edit-supervised-list.rb b/tests/user/edit-supervised-list.rb new file mode 100644 index 00000000..313ca544 --- /dev/null +++ b/tests/user/edit-supervised-list.rb @@ -0,0 +1,205 @@ +describe '/staff/supervisor-user-list' do + request('/user/logout') + + Scripts.createUser('supervisor@opensupports.com', 'passwordOfSupervisor', 'Supervisor Guy') + Scripts.createUser('usersupervised1@opensupports.com', 'usersupervised1', 'supervised Guy1') + Scripts.createUser('usersupervised2@opensupports.com', 'usersupervised2', 'supervised Guy2') + Scripts.createUser('usersupervised3@opensupports.com', 'usersupervised3', 'supervised Guy3') + + Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor') + Scripts.createTicket('titlecreateadbySUpervisor','contentoftitlecreateadbySUpervisor',1) + Scripts.logout() + + Scripts.login('usersupervised1@opensupports.com', 'usersupervised1') + Scripts.createTicket('titlecreateadbyusersupervised1','contentoftitlecreateadbyusersupervised1',3) + Scripts.logout() + + Scripts.login('usersupervised2@opensupports.com', 'usersupervised2') + Scripts.createTicket('titlecreateadbyusersupervised2','contentoftitlecreateadbyusersupervised2',1) + Scripts.logout() + + Scripts.login('usersupervised3@opensupports.com', 'usersupervised3') + Scripts.createTicket('titlecreateadbyusersupervised3','contentoftitlecreateadbyusersupervised3',1) + Scripts.logout() + + supervisor = $database.getRow('user', 'supervisor@opensupports.com', 'email') + user1 = $database.getRow('user', 'usersupervised1@opensupports.com', 'email') + user2 = $database.getRow('user', 'usersupervised2@opensupports.com', 'email') + user3 = $database.getRow('user', 'usersupervised3@opensupports.com', 'email') + + ticketsupervisor = $database.getRow('ticket', 'titlecreateadbySUpervisor', 'title') + ticketuser1 = $database.getRow('ticket', 'titlecreateadbyusersupervised1', 'title') + ticketuser2 = $database.getRow('ticket', 'titlecreateadbyusersupervised2', 'title') + ticketuser3 = $database.getRow('ticket', 'titlecreateadbyusersupervised3', 'title') + it'should fail if a no-staff tryes to make the request'do + request('/user/logout') + Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor') + + result = request('/user/edit-supervised-list', { + userIdList: "[30,31,32]", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('NO_PERMISSION') + + end + + it 'should fail if userIdList is wrong' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + result = request('/user/edit-supervised-list', { + userIdList: "1", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_LIST') + + result = request('/user/edit-supervised-list', { + userIdList: "array", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_LIST') + + result = request('/user/edit-supervised-list', { + userIdList: "[30,31,32,666666]", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_LIST') + end + + it'should fail if userId is wrong'do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + result = request('/user/edit-supervised-list', { + userIdList: "[30,31,32]", + userId: 666, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_USER') + end + + it'should fail if supervisor is included in user-id-List'do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + result = request('/user/edit-supervised-list', { + userIdList: "[30,31,29]", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('SUPERVISOR_CAN_NOT_SUPERVISE_HIMSELF') + end + + it'should create supervisor user'do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + result = request('/user/edit-supervised-list', { + userIdList: "[30,31,32]", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + + end + + it 'should allow supervisor to access tickets from supervisated users' do + request('/user/logout') + Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor') + result = request('/ticket/get', { + ticketNumber: ticketsupervisor['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + + result = request('/ticket/get', { + ticketNumber: ticketuser1['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + + result = request('/ticket/get', { + ticketNumber: ticketuser2['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + + result = request('/ticket/get', { + ticketNumber: ticketuser3['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + + end + it 'should allow supervisor see only the new user list' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + request('/user/edit-supervised-list', { + userIdList: "[30]", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + + request('/user/logout') + Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor') + + result = request('/ticket/get', { + ticketNumber: ticketsupervisor['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + + result = request('/ticket/get', { + ticketNumber: ticketuser1['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('success') + + result = request('/ticket/get', { + ticketNumber: ticketuser2['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('fail') + + result = request('/ticket/get', { + ticketNumber: ticketuser3['ticket_number'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + (result['status']).should.equal('fail') + end +end diff --git a/tests/user/get-supervised-tickets.rb b/tests/user/get-supervised-tickets.rb new file mode 100644 index 00000000..081cc7ac --- /dev/null +++ b/tests/user/get-supervised-tickets.rb @@ -0,0 +1,231 @@ +describe '/user/get-supervised-tickets' do + request('/user/logout') + + supervisor = $database.getRow('user', 'supervisor@opensupports.com', 'email') + user1 = $database.getRow('user', 'usersupervised1@opensupports.com', 'email') + user2 = $database.getRow('user', 'usersupervised2@opensupports.com', 'email') + + ticketsupervisor = $database.getRow('ticket', 'titlecreateadbySUpervisor', 'title') + ticketuser1 = $database.getRow('ticket', 'titlecreateadbyusersupervised1', 'title') + ticketuser2 = $database.getRow('ticket', 'titlecreateadbyusersupervised2', 'title') + ticketuser3 = $database.getRow('ticket', 'titlecreateadbyusersupervised3', 'title') + + + it 'should fail if supervised users are not valid' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + result = request('/user/edit-supervised-list', { + userIdList: "[30,32,31]", + userId: supervisor['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + + request('/user/logout') + Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[1000,30]", + showOwnTickets: 1, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[32,30,1]", + showOwnTickets: 1, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "32", + showOwnTickets: 1, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "hello", + showOwnTickets: 1, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[{'id' :29 , 'staff' true}]", + showOwnTickets: 1, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + end + + it 'should return the tickets of the authors searched' do + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[30,32,31]", + showOwnTickets: 0, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + (result['data']['tickets'].size).should.equal(3) + (result['data']['tickets'][0]['title']).should.equal(ticketuser3['title']) + (result['data']['tickets'][1]['title']).should.equal(ticketuser2['title']) + (result['data']['tickets'][2]['title']).should.equal(ticketuser1['title']) + end + it 'should return the tickets of the authors searched including logged user' do + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[30,32]", + showOwnTickets: 1, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + (result['data']['tickets'].size).should.equal(3) + (result['data']['tickets'][0]['title']).should.equal(ticketuser3['title']) + (result['data']['tickets'][1]['title']).should.equal(ticketuser1['title']) + (result['data']['tickets'][2]['title']).should.equal(ticketsupervisor['title']) + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[30,32,29]", + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + (result['data']['tickets'].size).should.equal(3) + (result['data']['tickets'][0]['title']).should.equal(ticketuser3['title']) + (result['data']['tickets'][1]['title']).should.equal(ticketuser1['title']) + (result['data']['tickets'][2]['title']).should.equal(ticketsupervisor['title']) + end + + it 'should return empty list if supervised users is empty and show own tickets off' do + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[]", + showOwnTickets: 0, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + (result['data']).should.equal([]) + end + it 'should works propertly if 2 supervisors has the same users' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + Scripts.createUser('supervisor2@opensupports.com', 'usersupervised2', 'supervisor Guy2') + supervisor2 = $database.getRow('user', 'supervisor2@opensupports.com', 'email') + + result = request('/user/edit-supervised-list', { + userIdList: "[30,32,31]", + userId: supervisor2['id'], + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + Scripts.login('supervisor@opensupports.com', 'passwordOfSupervisor') + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[30,32,31]", + showOwnTickets: 0, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + (result['data']['tickets'].size).should.equal(3) + (result['data']['tickets'][0]['title']).should.equal(ticketuser3['title']) + (result['data']['tickets'][1]['title']).should.equal(ticketuser2['title']) + (result['data']['tickets'][2]['title']).should.equal(ticketuser1['title']) + Scripts.logout() + + Scripts.login('supervisor2@opensupports.com', 'usersupervised2') + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[30,32,31]", + showOwnTickets: 0, + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + (result['data']['tickets'].size).should.equal(3) + (result['data']['tickets'][0]['title']).should.equal(ticketuser3['title']) + (result['data']['tickets'][1]['title']).should.equal(ticketuser2['title']) + (result['data']['tickets'][2]['title']).should.equal(ticketuser1['title']) + Scripts.logout() + + end + + it 'should if supervised Users tryes to handle supervisor-ticket' do + request('/user/logout') + Scripts.login('usersupervised1@opensupports.com', 'usersupervised1') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[29]", + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + request('/user/logout') + Scripts.login('usersupervised2@opensupports.com', 'usersupervised2') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[29]", + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + request('/user/logout') + Scripts.login('usersupervised3@opensupports.com', 'usersupervised3') + + result = request('/user/get-supervised-tickets', { + supervisedUsers: "[29]", + page: 1, + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_SUPERVISED_USERS') + + end +end diff --git a/version_upgrades/4.3.2/models/MailTemplate.php b/version_upgrades/4.3.2/models/MailTemplate.php index f985dfe3..46c288cb 100644 --- a/version_upgrades/4.3.2/models/MailTemplate.php +++ b/version_upgrades/4.3.2/models/MailTemplate.php @@ -3,7 +3,7 @@ use RedBeanPHP\Facade as RedBean; /** * @api {OBJECT} MailTemplate MailTemplate - * @apiVersion 4.3.2 + * @apiVersion 4.7 * @apiGroup Data Structures * @apiParam {String} type The type of the mail template. * @apiParam {String} subject The subject of the mail template.