From b3638abf63abf96214c48b98e5b00c77e666f4da Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Mon, 22 Jul 2019 18:09:23 +0200 Subject: [PATCH] Added global error handling; Added add/rename/delete group functionalities --- gms-ui/package-lock.json | 7 +- gms-ui/package.json | 2 +- gms-ui/src/App.vue | 10 +++ gms-ui/src/api/server/index.js | 89 ++++++++++++++++++- gms-ui/src/components/GroupsPanel.vue | 25 ++++-- gms-ui/src/components/Main.vue | 15 ++-- .../modals/ConfirmDeleteGroupModal.vue | 33 +++++++ .../src/components/modals/NewGroupModal.vue | 65 ++++++++++++++ .../components/modals/RenameGroupModal.vue | 73 +++++++++++++++ gms-ui/src/plugins/bootstrap-vue.js | 11 +-- gms-ui/src/store.js | 13 ++- .../it/inaf/ia2/gms/authn/SecurityConfig.java | 2 + .../ia2/gms/controller/GroupsController.java | 54 ++++++++++- .../gms/exception/BadRequestException.java | 4 + .../gms/exception/UnauthorizedException.java | 4 + .../inaf/ia2/gms/model/BaseModelRequest.java | 14 --- .../ia2/gms/model/CreateGroupRequest.java | 20 ++--- .../java/it/inaf/ia2/gms/model/GroupNode.java | 3 - .../ia2/gms/model/GroupsModelRequest.java | 24 ++--- .../ia2/gms/model/PaginatedModelRequest.java | 26 ++++++ .../ia2/gms/model/RenameGroupRequest.java | 19 ++++ .../inaf/ia2/gms/service/GroupsService.java | 44 ++++++++- 22 files changed, 474 insertions(+), 83 deletions(-) create mode 100644 gms-ui/src/components/modals/ConfirmDeleteGroupModal.vue create mode 100644 gms-ui/src/components/modals/NewGroupModal.vue create mode 100644 gms-ui/src/components/modals/RenameGroupModal.vue delete mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/BaseModelRequest.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/PaginatedModelRequest.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/RenameGroupRequest.java diff --git a/gms-ui/package-lock.json b/gms-ui/package-lock.json index f41f0cb..e988bba 100644 --- a/gms-ui/package-lock.json +++ b/gms-ui/package-lock.json @@ -2367,13 +2367,12 @@ "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" }, "bootstrap-vue": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.0.0-rc.24.tgz", - "integrity": "sha512-8rA/I9tOvpNVIuMKD3rdlrUqgVdPEw4vPI0X8OeFJcG2hHvCHeZDF7FmWqxSeehIrUHGDV17HlTGSuP/v1Sp5g==", + "version": "2.0.0-rc.27", + "resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.0.0-rc.27.tgz", + "integrity": "sha512-gXdpt2IsKbmC3SU0bf/RgldWwgrXK7G47yslOtkk4OA1z6fOzb2orM2vU5L8NCNZQomYax9LapRucv+8PByfdA==", "requires": { "@nuxt/opencollective": "^0.2.2", "bootstrap": "^4.3.1", - "core-js": ">=2.6.5 <3.0.0", "popper.js": "^1.15.0", "portal-vue": "^2.1.5", "vue-functional-data-merge": "^3.1.0" diff --git a/gms-ui/package.json b/gms-ui/package.json index f963d0c..2b6cd4b 100644 --- a/gms-ui/package.json +++ b/gms-ui/package.json @@ -11,7 +11,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.19", "@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/vue-fontawesome": "^0.1.6", - "bootstrap-vue": "^2.0.0-rc.19", + "bootstrap-vue": "^2.0.0-rc.27", "core-js": "^2.6.5", "vue": "^2.6.10", "vuex": "^3.1.1" diff --git a/gms-ui/src/App.vue b/gms-ui/src/App.vue index c5635db..ed82340 100644 --- a/gms-ui/src/App.vue +++ b/gms-ui/src/App.vue @@ -11,6 +11,7 @@ import TopMenu from './components/TopMenu.vue'; import Main from './components/Main.vue'; import { mapState } from 'vuex'; +//import { ToastPlugin } from 'bootstrap-vue'; export default { name: 'app', @@ -23,6 +24,15 @@ export default { }), mounted: function() { this.$store.commit('fetchGroupsModel'); + + var self = this; + document.addEventListener('apiError', function (event) { + self.$bvToast.toast(event.message, { + title: "Error", + variant: 'danger', + solid: true + }); + }) } } </script> diff --git a/gms-ui/src/api/server/index.js b/gms-ui/src/api/server/index.js index 1fc5686..00c8879 100644 --- a/gms-ui/src/api/server/index.js +++ b/gms-ui/src/api/server/index.js @@ -1,9 +1,43 @@ const BASE_API_URL = "http://localhost:8081/" +function apiRequest(url, data) { + return new Promise((resolve, reject) => { + fetch(url, data) + .then(response => { + if([200, 201, 204, 400].includes(response.status)) { // valid status codes + resolve(response.json()); + } else { + response.json().then(jsonValue => dispatchApiErrorEvent(jsonValue)); + } + }) + .catch(error => { + dispatchApiErrorEvent(error); + }); + }); +} + +function dispatchApiErrorEvent(error) { + let message; + if(error.message) { + message = error.message; + } else { + message = 'Generic error'; + } + + let event = new CustomEvent('apiError'); + event.message = message; + document.dispatchEvent(event); +} + export default { fetchGroupsModel (input) { - let url = BASE_API_URL + 'groups?groupId=' + input.selectedGroupId + '&tab=' + input.selectedTab + '&paginatorPageSize=' + input.paginatorPageSize + '&paginatorPage=' + input.paginatorPage; - return fetch(url, { + let url = BASE_API_URL + + 'groups?groupId=' + input.selectedGroupId + + '&tab=' + input.selectedTab + + '&paginatorPageSize=' + input.paginatorPageSize + + '&paginatorPage=' + input.paginatorPage + + '&page=' + input.page; + return apiRequest(url, { method: 'GET', cache: 'no-cache', credentials: 'include', @@ -11,6 +45,55 @@ export default { 'Content-Type': 'application/json', 'Accept': 'application/json', } - }).then(response => response.json()); + }); + }, + addGroup (newGroupName, input) { + let url = BASE_API_URL + 'group'; + return apiRequest(url, { + method: 'POST', + cache: 'no-cache', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + newGroupName: newGroupName, + parentGroupId: input.selectedGroupId, + paginatorPageSize: input.paginatorPageSize, + paginatorPage: input.paginatorPage + }) + }); + }, + renameGroup (groupId, newGroupName, input) { + let url = BASE_API_URL + 'group/' + groupId; + return apiRequest(url, { + method: 'PUT', + cache: 'no-cache', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + newGroupName: newGroupName, + paginatorPageSize: input.paginatorPageSize, + paginatorPage: input.paginatorPage + }) + }); + }, + deleteGroup (groupId, input) { + let url = BASE_API_URL + 'group/' + groupId + + '?paginatorPageSize=' + input.paginatorPageSize + + '&paginatorPage=' + input.paginatorPage; + return apiRequest(url, { + method: 'DELETE', + cache: 'no-cache', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + }); } }; diff --git a/gms-ui/src/components/GroupsPanel.vue b/gms-ui/src/components/GroupsPanel.vue index 38fc4bc..bc37ed9 100644 --- a/gms-ui/src/components/GroupsPanel.vue +++ b/gms-ui/src/components/GroupsPanel.vue @@ -8,12 +8,13 @@ <div id="groups-list"> <b-list-group v-for="group in model.groupsPanel.items"> <b-list-group-item href="#" v-on:click="openGroup(group)"> - <span class="float-left">{{group.name}}</span> + <span class="float-left">{{group.groupName}}</span> <span v-if="group.permissions.includes('ADMIN')" class="float-right"> - <a href="#" v-on:click="renameGroup(group)"> + <a href="#" v-on:click.stop="openRenameGroupModal(group)" title="Rename"> <font-awesome-icon icon="edit"></font-awesome-icon> </a> - <a href="#" v-on:click="deleteGroup(group)"> + + <a href="#" v-on:click.stop="openDeleteGroupModal(group)" class="text-danger" title="Delete"> <font-awesome-icon icon="trash"></font-awesome-icon> </a> </span> @@ -46,14 +47,22 @@ </div> <div class="text-center"> </div> + <RenameGroupModal ref="renameGroupModal" /> + <ConfirmDeleteGroupModal ref="confirmDeleteGroupModal" /> </b-tab> </template> <script> +import RenameGroupModal from './modals/RenameGroupModal.vue' +import ConfirmDeleteGroupModal from './modals/ConfirmDeleteGroupModal.vue' import { mapState, mapActions } from 'vuex' export default { name: 'GroupsPanel', + components: { + RenameGroupModal, + ConfirmDeleteGroupModal + }, computed: mapState({ model: state => state.model, input: state => state.input @@ -70,14 +79,14 @@ export default { }, methods: { openGroup: function(group) { - this.$store.state.input.selectedGroupId = group.id; + this.$store.state.input.selectedGroupId = group.groupId; this.$store.commit('fetchGroupsModel'); }, - renameGroup: function(group) { - console.log('rename ' + group.id); + openRenameGroupModal: function(group) { + this.$refs.renameGroupModal.openRenameGroupModal(group); }, - deleteGroup: function(group) { - console.log('del ' + group.id); + openDeleteGroupModal: function(group) { + this.$refs.confirmDeleteGroupModal.openDeleteGroupModal(group); }, setPage: function(page) { console.log('setPage ', page); diff --git a/gms-ui/src/components/Main.vue b/gms-ui/src/components/Main.vue index 95aa9fb..87442ad 100644 --- a/gms-ui/src/components/Main.vue +++ b/gms-ui/src/components/Main.vue @@ -3,14 +3,14 @@ <GroupsBreadcrumb /> <div class=""> <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('ADMIN')">Add member</button> - <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('ADMIN')">Add group</button> - <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('ADMIN') || model.permissions.includes('MANAGE_MEMBERS')">Add collaborator</button> + <b-button variant="primary" class="float-right" v-if="model.permissions.includes('ADMIN')" v-b-modal.new-group-modal>Add group</b-button> + <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('MANAGE_MEMBERS')">Add collaborator</button> </div> <b-tabs content-class="mt-3"> <GroupsPanel /> <MembersPanel /> - </b-tab> </b-tabs> + <NewGroupModal /> </div> </template> @@ -18,6 +18,7 @@ import GroupsBreadcrumb from './GroupsBreadcrumb.vue' import GroupsPanel from './GroupsPanel.vue' import MembersPanel from './MembersPanel.vue' +import NewGroupModal from './modals/NewGroupModal.vue' import { mapState } from 'vuex'; export default { @@ -25,16 +26,14 @@ export default { components: { GroupsBreadcrumb, GroupsPanel, - MembersPanel + MembersPanel, + NewGroupModal }, computed: mapState({ model: state => state.model }), methods: { - addGroup: function() { - - }, - addMemeber: function() { + addMember: function() { } } diff --git a/gms-ui/src/components/modals/ConfirmDeleteGroupModal.vue b/gms-ui/src/components/modals/ConfirmDeleteGroupModal.vue new file mode 100644 index 0000000..05d2183 --- /dev/null +++ b/gms-ui/src/components/modals/ConfirmDeleteGroupModal.vue @@ -0,0 +1,33 @@ +<template> + <b-modal id="confirm-delete-group-modal" title="Confirm action" ok-title="Delete" @ok="deleteGroup" ok-variant="danger"> + <b-form inline> + <p v-if="groupToDelete">Are you sure that you want to delete the group <strong>{{groupToDelete.groupName}}</strong>?</p> + </b-form> + </b-modal> +</template> + +<script> +import client from 'api-client'; + +export default { + name: 'ConfirmDeleteGroupModal', + data: function() { + return { + groupToDelete: null + } + }, + methods: { + openDeleteGroupModal: function(group) { + this.groupToDelete = group; + this.$bvModal.show('confirm-delete-group-modal'); + }, + deleteGroup: function() { + client.deleteGroup(this.groupToDelete.groupId, this.$store.state.input) + .then(res => { + this.$store.commit('updateGroupsPanel', res); + this.$bvModal.hide('confirm-delete-group-modal'); + }) + } + } +} +</script> diff --git a/gms-ui/src/components/modals/NewGroupModal.vue b/gms-ui/src/components/modals/NewGroupModal.vue new file mode 100644 index 0000000..e976b8c --- /dev/null +++ b/gms-ui/src/components/modals/NewGroupModal.vue @@ -0,0 +1,65 @@ +<template> + <b-modal id="new-group-modal" title="Add group" @show="resetModal" ok-title="Add" @ok="addGroup"> + <b-form inline> + <label class="w-25" for="new-group-name-input">Group name:</label> + <b-form-input v-model="newGroupName" + id="new-group-name-input" + class="w-75" + aria-describedby="new-group-name-input-feedback" + :state="newGroupNameState" + v-on:input="resetError"> + </b-form-input> + <b-form-invalid-feedback id="new-group-name-input-feedback">{{newGroupNameError}}</b-form-invalid-feedback> + </b-form> + </b-modal> +</template> + +<script> +import client from 'api-client'; + +export default { + name: 'NewGroupModal', + computed: { + newGroupNameState() { + if(this.newGroupNameError) { + return 'invalid'; + } + } + }, + data: function() { + return { + newGroupName: null, + newGroupNameError: null + }; + }, + methods: { + resetModal: function() { + this.newGroupName = null; + this.resetError(); + }, + resetError: function() { + this.newGroupNameError = null; + }, + addGroup: function(event) { + // Prevent modal from closing + event.preventDefault(); + + if(!this.newGroupName) { + this.newGroupNameError = "Group name is required"; + return; + } + + let parent = this.$store.getters.selectedGroupId; + client.addGroup(this.newGroupName, this.$store.state.input) + .then(res => { + if(res.status === 400) { + this.newGroupNameError = res.message; + } else { + this.$store.commit('updateGroupsPanel', res); + this.$bvModal.hide('new-group-modal'); + } + }); + } + } +} +</script> diff --git a/gms-ui/src/components/modals/RenameGroupModal.vue b/gms-ui/src/components/modals/RenameGroupModal.vue new file mode 100644 index 0000000..42af1b9 --- /dev/null +++ b/gms-ui/src/components/modals/RenameGroupModal.vue @@ -0,0 +1,73 @@ +<template> + <b-modal id="rename-group-modal" title="Rename group" ok-title="Rename" @ok="renameGroup"> + <b-form inline> + <label class="w-25" for="new-group-name-input">Group name:</label> + <b-form-input v-model="newGroupName" + id="new-group-name-input" + class="w-75" + aria-describedby="new-group-name-input-feedback" + :state="newGroupNameState" + v-on:input="resetError"> + </b-form-input> + <b-form-invalid-feedback id="new-group-name-input-feedback">{{newGroupNameError}}</b-form-invalid-feedback> + </b-form> + </b-modal> +</template> + +<script> +import client from 'api-client'; + +export default { + name: 'RenameGroupModal', + computed: { + newGroupNameState() { + if(this.newGroupNameError) { + return 'invalid'; + } + } + }, + data: function() { + return { + groupId: null, + oldGroupName: null, + newGroupName: null, + newGroupNameError: null + }; + }, + methods: { + resetError: function() { + this.newGroupNameError = null; + }, + openRenameGroupModal: function(group) { + this.newGroupName = group.groupName; + this.groupId = group.groupId; + this.$bvModal.show('rename-group-modal'); + }, + renameGroup: function(event) { + // Prevent modal from closing + event.preventDefault(); + + if(!this.newGroupName) { + this.newGroupNameError = "Group name is required"; + return; + } + + if(this.oldGroupName === this.newGroupName) { + this.$bvModal.hide('rename-group-modal'); + return; + } + + let parent = this.$store.getters.selectedGroupId; + client.renameGroup(this.groupId, this.newGroupName, this.$store.state.input) + .then(res => { + if(res.status === 400) { + this.newGroupNameError = res.message; + } else { + this.$store.commit('updateGroupsPanel', res); + this.$bvModal.hide('rename-group-modal'); + } + }); + } + } +} +</script> diff --git a/gms-ui/src/plugins/bootstrap-vue.js b/gms-ui/src/plugins/bootstrap-vue.js index 7ad0b7c..c29a066 100644 --- a/gms-ui/src/plugins/bootstrap-vue.js +++ b/gms-ui/src/plugins/bootstrap-vue.js @@ -1,7 +1,8 @@ -import Vue from 'vue' +import Vue from 'vue'; -import BootstrapVue from 'bootstrap-vue' -import 'bootstrap/dist/css/bootstrap.min.css' -import 'bootstrap-vue/dist/bootstrap-vue.css' +import { BootstrapVue } from 'bootstrap-vue'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap-vue/dist/bootstrap-vue.css'; -Vue.use(BootstrapVue) +Vue.use(BootstrapVue); +//Vue.use(ToastPlugin); diff --git a/gms-ui/src/store.js b/gms-ui/src/store.js index dcf9906..c6e9593 100644 --- a/gms-ui/src/store.js +++ b/gms-ui/src/store.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import client from 'api-client' +import client from 'api-client'; Vue.use(Vuex); @@ -15,7 +15,8 @@ export default new Vuex.Store({ selectedGroupId: 'ROOT', paginatorPageSize: 20, paginatorPage: 1, - selectedTab: 'groups' + selectedTab: 'groups', + page: 'main' } }, mutations: { @@ -24,6 +25,14 @@ export default new Vuex.Store({ .then(model => { this.state.model = model; }); + }, + updateGroupsPanel(state, groupsPanel) { + this.state.model.groupsPanel = groupsPanel; + } + }, + getters: { + selectedGroupId: state => { + return state.model.breadcrumbs[state.model.breadcrumbs.length - 1].groupId; } } }); diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java index 42fa738..c1b4c6a 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java +++ b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java @@ -45,6 +45,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues(); + config.addAllowedMethod(HttpMethod.PUT); + config.addAllowedMethod(HttpMethod.DELETE); config.setAllowedOrigins(Arrays.asList(corsAllowedOrigin)); config.setAllowCredentials(true); source.registerCorsConfiguration("/**", config); diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java index 4ee7683..256cc9a 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java @@ -1,21 +1,32 @@ package it.inaf.ia2.gms.controller; import it.inaf.ia2.gms.authn.SessionData; +import it.inaf.ia2.gms.exception.BadRequestException; import it.inaf.ia2.gms.model.CreateGroupRequest; +import it.inaf.ia2.gms.model.GroupNode; import it.inaf.ia2.gms.model.GroupsModelRequest; import it.inaf.ia2.gms.model.GroupsModelResponse; +import it.inaf.ia2.gms.model.PaginatedData; +import it.inaf.ia2.gms.model.PaginatedModelRequest; +import it.inaf.ia2.gms.model.RenameGroupRequest; import it.inaf.ia2.gms.persistence.model.Group; import it.inaf.ia2.gms.persistence.model.User; import it.inaf.ia2.gms.service.GroupsModelService; import it.inaf.ia2.gms.service.GroupsService; import it.inaf.ia2.gms.service.UsersService; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -40,9 +51,44 @@ public class GroupsController { } @PostMapping(value = "/group", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public ResponseEntity<String> createGroup(@Valid @RequestBody CreateGroupRequest createGroupRequest) { - User user = usersService.getUserById(session.getUserId()); - Group group = groupsService.addGroup(createGroupRequest.getParentGroupId(), createGroupRequest.getGroupName(), user); - return new ResponseEntity<>(group.getId(), HttpStatus.CREATED); + public ResponseEntity<PaginatedData<GroupNode>> createGroup(@Valid @RequestBody CreateGroupRequest request) { + + User user = getUser(); + Group newGroup = groupsService.addGroup(request.getParentGroupId(), request.getNewGroupName(), user); + + PaginatedData<GroupNode> groupsPanel = getGroupsPanel(newGroup.getParentGroup(), request, user); + + return new ResponseEntity<>(groupsPanel, HttpStatus.CREATED); + } + + @PutMapping(value = "/group/{groupId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity<PaginatedData<GroupNode>> renameGroup(@PathVariable("groupId") String groupId, @Valid @RequestBody RenameGroupRequest request) { + + User user = getUser(); + Group renamedGroup = groupsService.renameGroup(groupId, request.getNewGroupName(), user); + + PaginatedData<GroupNode> groupsPanel = getGroupsPanel(renamedGroup.getParentGroup(), request, user); + + return ResponseEntity.ok(groupsPanel); + } + + @DeleteMapping(value = "/group/{groupId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity<?> deleteGroup(@PathVariable("groupId") String groupId, PaginatedModelRequest request) { + + User user = getUser(); + Group parent = groupsService.deleteGroup(groupId, user); + + PaginatedData<GroupNode> groupsPanel = getGroupsPanel(parent, request, user); + + return ResponseEntity.ok(groupsPanel); + } + + private User getUser() { + return usersService.getUserById(session.getUserId()); + } + + private PaginatedData<GroupNode> getGroupsPanel(Group parentGroup, PaginatedModelRequest paginatedRequest, User user) { + List<GroupNode> groupNodes = groupsService.getSubgroups(parentGroup, user); + return new PaginatedData<>(groupNodes, paginatedRequest.getPaginatorPage(), paginatedRequest.getPaginatorPageSize()); } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java index 039ecfe..50f633f 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java +++ b/gms/src/main/java/it/inaf/ia2/gms/exception/BadRequestException.java @@ -1,5 +1,9 @@ package it.inaf.ia2.gms.exception; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) public class BadRequestException extends RuntimeException { public BadRequestException(String message) { diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java index 2b07d8c..e4f73db 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java +++ b/gms/src/main/java/it/inaf/ia2/gms/exception/UnauthorizedException.java @@ -1,5 +1,9 @@ package it.inaf.ia2.gms.exception; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.UNAUTHORIZED) public class UnauthorizedException extends RuntimeException { public UnauthorizedException(String message) { diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/BaseModelRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/BaseModelRequest.java deleted file mode 100644 index 1330e69..0000000 --- a/gms/src/main/java/it/inaf/ia2/gms/model/BaseModelRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package it.inaf.ia2.gms.model; - -public abstract class BaseModelRequest { - - private String page; - - public String getPage() { - return page; - } - - public void setPage(String page) { - this.page = page; - } -} diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java index a62c852..c7444a9 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java +++ b/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java @@ -1,23 +1,23 @@ package it.inaf.ia2.gms.model; -public class CreateGroupRequest { +public class CreateGroupRequest extends PaginatedModelRequest { private String parentGroupId; - private String groupName; + private String newGroupName; - public String getParentGroupId() { - return parentGroupId; + public String getNewGroupName() { + return newGroupName; } - public void setParentGroupId(String parentGroupId) { - this.parentGroupId = parentGroupId; + public void setNewGroupName(String newGroupName) { + this.newGroupName = newGroupName; } - public String getGroupName() { - return groupName; + public String getParentGroupId() { + return parentGroupId; } - public void setGroupName(String groupName) { - this.groupName = groupName; + public void setParentGroupId(String parentGroupId) { + this.parentGroupId = parentGroupId; } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java b/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java index ccdaf28..6ca71a1 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java +++ b/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java @@ -1,6 +1,5 @@ package it.inaf.ia2.gms.model; -import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -18,7 +17,6 @@ public class GroupNode { permissions = new HashSet<>(); } - @JsonProperty("id") public String getGroupId() { return groupId; } @@ -27,7 +25,6 @@ public class GroupNode { this.groupId = groupId; } - @JsonProperty("name") public String getGroupName() { return groupName; } diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/GroupsModelRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/GroupsModelRequest.java index 79e22f4..5c7a393 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/model/GroupsModelRequest.java +++ b/gms/src/main/java/it/inaf/ia2/gms/model/GroupsModelRequest.java @@ -1,9 +1,8 @@ package it.inaf.ia2.gms.model; -import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; -public class GroupsModelRequest extends BaseModelRequest { +public class GroupsModelRequest extends PaginatedModelRequest { @NotNull private String groupId; @@ -11,9 +10,8 @@ public class GroupsModelRequest extends BaseModelRequest { @NotNull private String tab; - @Min(1) - private int paginatorPage; - private int paginatorPageSize; + @NotNull + private String page; public String getGroupId() { return groupId; @@ -31,19 +29,11 @@ public class GroupsModelRequest extends BaseModelRequest { this.tab = tab; } - public int getPaginatorPage() { - return paginatorPage; - } - - public void setPaginatorPage(int paginatorPage) { - this.paginatorPage = paginatorPage; - } - - public int getPaginatorPageSize() { - return paginatorPageSize; + public String getPage() { + return page; } - public void setPaginatorPageSize(int paginatorPageSize) { - this.paginatorPageSize = paginatorPageSize; + public void setPage(String page) { + this.page = page; } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/PaginatedModelRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/PaginatedModelRequest.java new file mode 100644 index 0000000..371b90b --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/model/PaginatedModelRequest.java @@ -0,0 +1,26 @@ +package it.inaf.ia2.gms.model; + +import javax.validation.constraints.Min; + +public class PaginatedModelRequest { + + @Min(1) + private int paginatorPage; + private int paginatorPageSize; + + public int getPaginatorPage() { + return paginatorPage; + } + + public void setPaginatorPage(int paginatorPage) { + this.paginatorPage = paginatorPage; + } + + public int getPaginatorPageSize() { + return paginatorPageSize; + } + + public void setPaginatorPageSize(int paginatorPageSize) { + this.paginatorPageSize = paginatorPageSize; + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/RenameGroupRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/RenameGroupRequest.java new file mode 100644 index 0000000..90feff7 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/model/RenameGroupRequest.java @@ -0,0 +1,19 @@ +package it.inaf.ia2.gms.model; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public class RenameGroupRequest extends PaginatedModelRequest { + + @NotNull + @Size(min = 1) + private String newGroupName; + + public String getNewGroupName() { + return newGroupName; + } + + public void setNewGroupName(String newGroupName) { + this.newGroupName = newGroupName; + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java index 4be39d1..c82324a 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java +++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java @@ -53,15 +53,15 @@ public class GroupsService { Group parent = getGroupById(parentId); + if (!getPermissions(parent, user).contains(Permission.ADMIN)) { + throw new UnauthorizedException("Missing admin privileges"); + } + if (parent.getChildrenGroups().stream() .anyMatch(g -> g.getName().equals(groupName))) { throw new BadRequestException("There is already a group named " + groupName); } - if (!getPermissions(parent, user).contains(Permission.ADMIN)) { - throw new UnauthorizedException("Missing admin privileges"); - } - Group group = new Group(); group.setId(UUID.randomUUID().toString()); group.setName(groupName); @@ -72,6 +72,42 @@ public class GroupsService { return group; } + public Group renameGroup(String groupId, String newGroupName, User user) { + + Group group = getGroupById(groupId); + + if (!getPermissions(group, user).contains(Permission.ADMIN)) { + throw new UnauthorizedException("Missing admin privileges"); + } + + if (group.getParentGroup().getChildrenGroups().stream() + .anyMatch(g -> g.getName().equals(newGroupName))) { + throw new BadRequestException("There is already a group named " + newGroupName); + } + + group.setName(newGroupName); + return groupsRepository.save(group); + } + + public Group deleteGroup(String groupId, User user) { + + Group group = getGroupById(groupId); + + if (!getPermissions(group, user).contains(Permission.ADMIN)) { + throw new UnauthorizedException("Missing admin privileges"); + } + + if (ROOT.equals(group.getId())) { + throw new UnauthorizedException("It is not possible to remove the ROOT"); + } + + Group parent = group.getParentGroup(); + + groupsRepository.delete(group); + + return parent; + } + @Transactional public List<GroupNode> getSubgroups(Group parent, User user) { -- GitLab