Commit 1e9783fd authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added panel showing the pending invited registration requests; Added lock...

Added panel showing the pending invited registration requests; Added lock attribute on groups to avoid removal of special groups; Other improvements
parent 6c395713
......@@ -45,7 +45,9 @@
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"rules": {
"no-console": "warn"
},
"parserOptions": {
"parser": "babel-eslint"
}
......
......@@ -17,7 +17,7 @@ function apiRequest(options, showLoading = true, handleValidationErrors = false)
loading(false);
})
.catch(error => {
if(handleValidationErrors && error.response && error.response.status === 400) {
if (handleValidationErrors && error.response && error.response.status === 400) {
reject(error.response.data);
} else {
dispatchApiErrorEvent(error);
......@@ -363,5 +363,19 @@ export default {
'Cache-Control': 'no-cache'
}
}, false);
},
deleteInvitedRegistration(requestId, groupId) {
let url = BASE_API_URL + 'registration?' +
'request_id=' + requestId + '&group_id=' + groupId;
return apiRequest({
method: 'DELETE',
url: url,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Cache-Control': 'no-cache'
}
});
}
};
<template>
<div class="mt-sm-3">
<div>
<p>Search results:</p>
<b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id">
<b-list-group-item href="#" v-on:click="openSearchResult(item)">
<span class="float-left">
<font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon>
<font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon>
{{item.label}}
</span>
</b-list-group-item>
</b-list-group>
<Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" :paginatorInput="input.genericSearch" />
<div v-if="model.genericSearchResults.items && model.genericSearchResults.items.length > 0">
<p>Search results:</p>
<b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id">
<b-list-group-item href="#" v-on:click="openSearchResult(item)">
<span class="float-left">
<font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon>
<font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon>
{{item.label}}
</span>
</b-list-group-item>
</b-list-group>
<Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" :paginatorInput="input.genericSearch" />
</div>
</div>
<p v-if="model.genericSearchResults.items && model.genericSearchResults.items.length === 0">No entries were found matching your search</p>
</div>
</template>
......@@ -29,7 +32,7 @@ export default {
model: state => state.model,
input: state => state.input
}),
created () {
created() {
this.updateSearchResults();
},
watch: {
......
......@@ -14,7 +14,7 @@
<font-awesome-icon icon="edit"></font-awesome-icon>
</a>
&nbsp;
<a href="#" v-on:click.stop.prevent="openRemoveGroupModal(group)" class="text-danger" title="Delete">
<a href="#" v-on:click.stop.prevent="openRemoveGroupModal(group)" class="text-danger" title="Delete" v-if="!group.locked">
<font-awesome-icon icon="trash"></font-awesome-icon>
</a>
</span>
......
<template>
<b-tab :title="'Invited (' + registrations.length + ')'" :title-link-class="{ 'd-none': registrations.length === 0 }">
<table class="table b-table table-striped table-hover" v-if="registrations">
<thead>
<tr>
<th>Email</th>
<th>Permission</th>
<th>Submitted at</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(reg, index) in registrations" v-bind:key="index">
<td>{{reg.email}}</td>
<td>{{reg.permission}}</td>
<td>{{reg.creationTime}}</td>
<td>
<a href="#" v-on:click.stop="openConfirmDeleteInvitedModal(reg)" class="text-danger" title="Remove permission">
<font-awesome-icon icon="trash"></font-awesome-icon>
</a>
</td>
</tr>
</tbody>
</table>
<ConfirmDeleteInvitedModal ref="confirmDeleteInvitedModal" />
</b-tab>
</template>
<script>
import ConfirmDeleteInvitedModal from './modals/ConfirmDeleteInvitedModal.vue';
export default {
name: 'InvitedRegistrationPanel',
components: {
ConfirmDeleteInvitedModal
},
computed: {
registrations() {
return this.$store.state.model.invitedRegistrations === null ? [] : this.$store.state.model.invitedRegistrations;
}
},
methods: {
openConfirmDeleteInvitedModal(reg) {
let breadcrumbs = this.$store.state.model.breadcrumbs;
let currentGroupId = breadcrumbs[breadcrumbs.length - 1].groupId;
this.$refs.confirmDeleteInvitedModal.openConfirmDeleteInvitedModal(reg, currentGroupId);
}
}
}
</script>
......@@ -6,6 +6,7 @@
<GroupsPanel />
<MembersPanel />
<PermissionsPanel />
<InvitedRegistrationPanel />
<template slot="tabs-end">
<b-button variant="primary" class="in-tabs-header-btn" v-if="showAddGroupBtn" v-on:click="openAddGroupModal">Add group</b-button>
<b-button variant="primary" class="in-tabs-header-btn" v-if="showAddMemberBtn" v-on:click="openAddMemberModal">Add member</b-button>
......@@ -25,6 +26,7 @@ import GroupsBreadcrumb from './GroupsBreadcrumb.vue'
import GroupsPanel from './GroupsPanel.vue'
import MembersPanel from './MembersPanel.vue'
import PermissionsPanel from './PermissionsPanel.vue'
import InvitedRegistrationPanel from './InvitedRegistrationPanel.vue'
import AddGroupModal from './modals/AddGroupModal.vue'
import AddMemberModal from './modals/AddMemberModal.vue'
import AddPermissionModal from './modals/AddPermissionModal.vue'
......@@ -37,6 +39,7 @@ export default {
GroupsPanel,
MembersPanel,
PermissionsPanel,
InvitedRegistrationPanel,
AddGroupModal,
AddMemberModal,
AddPermissionModal
......
<template>
<b-tab title="Permissions" :title-link-class="{ 'd-none': (model.permission !== 'ADMIN') }">
<div v-if="model.permissionsPanel !== null">
<table class="table b-table table-striped table-hover">
<table class="table b-table table-striped table-hover" v-if="model.permissionsPanel.items.length > 0">
<thead>
<tr>
<th>User</th>
......
......@@ -29,7 +29,7 @@
<b-row>
<b-col lg="10">
<b-list-group>
<b-list-group-item v-for="(identity, index) in user.identities" v-bind:key="index">
<b-list-group-item :class="{ 'list-group-item-info': identity.primary }" v-for="(identity, index) in user.identities" v-bind:key="index">
<dl class="mb-0 ml-0 row">
<dt class="col-3">Type</dt><dd class="col-9">{{identity.type}}</dd>
<dt class="col-3">Email</dt><dd class="col-9">{{identity.email}}</dd>
......
<template>
<b-modal id="add-member-modal" title="Add member" ok-title="Add" @shown="afterShow" @ok="addMember">
<b-modal id="add-member-modal" title="Add member" ok-title="Add" @shown="afterShow" @ok="addMember" size="lg">
<SearchUser ref="searchUser" @searchUserEnter="addMember" />
</b-modal>
</template>
......
<template>
<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant">
<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant" size="lg">
<SearchUser ref="searchUser" @searchUserEnter="addPermission" />
<b-alert :show="!!existingPermission" variant="warning" class="mt-3">
<strong>Warning</strong>: the user has already a permission ({{existingPermission}}). Click confirm to override it.
......
<template>
<b-modal id="confirm-delete-invited-modal" title="Confirm action" ok-title="Delete" @ok="deleteInvitedRegistration" ok-variant="danger">
<p v-if="invitedRegistration">Are you sure that you want to remove the invited registration for {{invitedRegistration.email}}?</p>
</b-modal>
</template>
<script>
import client from 'api-client';
export default {
name: 'ConfirmDeleteInvitedModal',
data: function() {
return {
invitedRegistration: null,
groupId: null
}
},
methods: {
openConfirmDeleteInvitedModal(invitedRegistration, groupId) {
this.invitedRegistration = invitedRegistration;
this.groupId = groupId;
this.$bvModal.show('confirm-delete-invited-modal');
},
deleteInvitedRegistration() {
let regId = this.invitedRegistration.id;
client.deleteInvitedRegistration(regId, this.groupId)
.then(() => {
let model = this.$store.state.model;
this.$store.commit('removeInvitedRegistration', regId);
if (model.invitedRegistrations.length === 0) {
this.$store.commit('setTabIndex', model.leaf ? 1 : 0);
}
});
}
}
}
</script>
......@@ -50,7 +50,7 @@ export default {
let user = res[i];
this.users.push({
value: user.id,
text: user.displayName
text: user.displayName + ' [' + user.id + ']'
});
}
if (this.users.length > 0) {
......
......@@ -15,6 +15,7 @@ export default new Vuex.Store({
groupsPanel: null,
permissionsPanel: null,
membersPanel: null,
invitedRegistrations: null,
permission: null,
leaf: false,
user: null,
......@@ -45,12 +46,14 @@ export default new Vuex.Store({
state.model.breadcrumbs = model.breadcrumbs;
state.model.groupsPanel = model.groupsPanel;
state.model.permission = model.permission;
state.model.invitedRegistrations = model.invitedRegistrations;
state.model.user = model.user;
},
updateGroups(state, model) {
state.model.breadcrumbs = model.breadcrumbs;
state.model.groupsPanel = model.groupsPanel;
state.model.permission = model.permission;
state.model.invitedRegistrations = model.invitedRegistrations;
state.model.leaf = model.leaf;
},
updateGroupsPanel(state, groupsPanel) {
......@@ -59,7 +62,7 @@ export default new Vuex.Store({
},
updatePermissionsPanel(state, permissionsPanel) {
state.model.permissionsPanel = permissionsPanel;
for(let up of permissionsPanel.items) {
for (let up of permissionsPanel.items) {
Vue.set(up, 'editable', false);
}
state.input.paginatorPage = permissionsPanel.currentPage;
......@@ -94,6 +97,18 @@ export default new Vuex.Store({
},
setGenericSearchFilter(state, filter) {
state.input.genericSearch.filter = filter;
},
removeInvitedRegistration(state, regId) {
let index = -1;
for (var i = 0; i < state.model.invitedRegistrations.length; i++) {
if (state.model.invitedRegistrations[i].id === regId) {
index = i;
break;
}
}
if (index !== -1) {
state.model.invitedRegistrations.splice(index, 1);
}
}
},
actions: {
......
......@@ -2,6 +2,7 @@ package it.inaf.ia2.gms.controller;
import it.inaf.ia2.gms.authn.SessionData;
import it.inaf.ia2.gms.manager.GroupsManager;
import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
import it.inaf.ia2.gms.manager.PermissionsManager;
import it.inaf.ia2.gms.model.Permission;
import it.inaf.ia2.gms.model.request.GroupsRequest;
......@@ -30,6 +31,9 @@ public class GroupsTabResponseBuilder {
@Autowired
private GroupsTreeBuilder groupsListBuilder;
@Autowired
private InvitedRegistrationManager invitedRegistrationManager;
public GroupsTabResponse getGroupsTab(GroupsRequest request) {
GroupEntity group = groupsService.getGroupById(request.getGroupId());
......@@ -46,6 +50,8 @@ public class GroupsTabResponseBuilder {
response.setLeaf(group.isLeaf());
response.setInvitedRegistrations(invitedRegistrationManager.getInvitedRegistrationsForGroup(group));
return response;
}
}
......@@ -10,7 +10,9 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
......@@ -55,6 +57,12 @@ public class InvitedRegistrationController {
}
}
@DeleteMapping(value = "/registration", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> deleteInvitedRegistration(@RequestParam("request_id") String requestId, @RequestParam("group_id") String groupId) {
invitedRegistrationManager.deleteInvitedRegistration(requestId, groupId);
return ResponseEntity.noContent().build();
}
private String getFileContent(String templateFileName) throws IOException {
try (InputStream in = InvitedRegistrationController.class.getClassLoader().getResourceAsStream("templates/" + templateFileName)) {
Scanner s = new Scanner(in).useDelimiter("\\A");
......
package it.inaf.ia2.gms.manager;
import it.inaf.ia2.gms.exception.BadRequestException;
import it.inaf.ia2.gms.exception.NotFoundException;
import it.inaf.ia2.gms.exception.UnauthorizedException;
import it.inaf.ia2.gms.model.Permission;
import it.inaf.ia2.gms.model.response.InvitedRegistrationItem;
import it.inaf.ia2.gms.persistence.GroupsDAO;
import it.inaf.ia2.gms.persistence.InvitedRegistrationDAO;
import it.inaf.ia2.gms.persistence.LoggingDAO;
......@@ -14,8 +16,10 @@ import it.inaf.ia2.gms.service.PermissionsService;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
......@@ -119,4 +123,47 @@ public class InvitedRegistrationManager extends UserAwareComponent {
return Optional.ofNullable(invitedRegistration);
}
public List<InvitedRegistrationItem> getInvitedRegistrationsForGroup(GroupEntity group) {
if (permissionsManager.getCurrentUserPermission(group) != Permission.ADMIN) {
return null;
}
List<InvitedRegistrationItem> items = new ArrayList<>();
for (InvitedRegistration reg : invitedRegistrationDAO.getPendingInvitedRegistrationsForGroup(group.getId())) {
Map<String, Permission> map = reg.getGroupsPermissions();
if (map != null) {
for (Permission permission : map.values()) {
InvitedRegistrationItem item = new InvitedRegistrationItem()
.setId(reg.getId())
.setEmail(reg.getEmail())
.setPermission(permission)
.setCreationTime(reg.getCreationTime());
items.add(item);
}
}
}
return items;
}
public void deleteInvitedRegistration(String registrationId, String groupId) {
GroupEntity group = groupsDAO.findGroupById(groupId)
.orElseThrow(() -> new BadRequestException("No group found for given id: " + groupId));
if (permissionsManager.getUserPermission(group, getCurrentUserId()) != Permission.ADMIN) {
throw new UnauthorizedException("Only administrators can delete invited registrations!");
}
invitedRegistrationDAO.deleteInvitedRegistrationRequest(registrationId, groupId);
loggingDAO.logAction("Deleted invited registration request. "
+ "[request_id=" + registrationId + ", group_id=" + groupId
+ ", group_name=" + group.getName() + "]");
}
}
......@@ -7,6 +7,7 @@ public class GroupNode {
private Permission permission;
private boolean hasChildren;
private boolean leaf;
private boolean locked;
public String getGroupId() {
return groupId;
......@@ -47,4 +48,12 @@ public class GroupNode {
public void setLeaf(boolean leaf) {
this.leaf = leaf;
}
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
}
......@@ -11,6 +11,7 @@ public class GroupsTabResponse {
private PaginatedData<GroupNode> groupsPanel;
// current group permissions
private Permission permission;
private List<InvitedRegistrationItem> invitedRegistrations;
private boolean leaf;
......@@ -45,4 +46,12 @@ public class GroupsTabResponse {
public void setLeaf(boolean leaf) {
this.leaf = leaf;
}
public List<InvitedRegistrationItem> getInvitedRegistrations() {
return invitedRegistrations;
}
public void setInvitedRegistrations(List<InvitedRegistrationItem> invitedRegistrations) {
this.invitedRegistrations = invitedRegistrations;
}
}
package it.inaf.ia2.gms.model.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import it.inaf.ia2.gms.model.Permission;
import java.util.Date;
public class InvitedRegistrationItem {
private String id;
private String email;
private Permission permission;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date creationTime;
public String getId() {
return id;
}
public InvitedRegistrationItem setId(String id) {
this.id = id;
return this;
}
public String getEmail() {
return email;
}
public InvitedRegistrationItem setEmail(String email) {
this.email = email;
return this;
}
public Permission getPermission() {
return permission;
}
public InvitedRegistrationItem setPermission(Permission permission) {
this.permission = permission;
return this;
}
public Date getCreationTime() {
return creationTime;
}
public InvitedRegistrationItem setCreationTime(Date creationTime) {
this.creationTime = creationTime;
return this;
}
}
......@@ -84,7 +84,7 @@ public class GroupsDAO {
public Optional<GroupEntity> findGroupById(String groupId) {
String sql = "SELECT id, name, path, is_leaf from gms_group WHERE id = ?";
String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id = ?";
return jdbcTemplate.query(conn -> {
PreparedStatement ps = conn.prepareStatement(sql);
......@@ -97,6 +97,7 @@ public class GroupsDAO {
group.setName(resultSet.getString("name"));
group.setPath(resultSet.getString("path"));
group.setLeaf(resultSet.getBoolean("is_leaf"));
group.setLocked(resultSet.getBoolean("locked"));
return Optional.of(group);
}
return Optional.empty();
......@@ -105,7 +106,7 @@ public class GroupsDAO {
public Optional<GroupEntity> findGroupByPath(String path) {
String sql = "SELECT id, name, is_leaf from gms_group WHERE path = ?";
String sql = "SELECT id, name, is_leaf, locked from gms_group WHERE path = ?";
return jdbcTemplate.query(conn -> {
PreparedStatement ps = conn.prepareStatement(sql);
......@@ -117,6 +118,7 @@ public class GroupsDAO {
group.setId(resultSet.getString("id"));
group.setName(resultSet.getString("name"));
group.setLeaf(resultSet.getBoolean("is_leaf"));
group.setLocked(resultSet.getBoolean("locked"));
group.setPath(path);
return Optional.of(group);
}
......@@ -132,7 +134,7 @@ public class GroupsDAO {
return jdbcTemplate.query(conn -> {
String sql = "SELECT id, name, path, is_leaf from gms_group WHERE id IN (";
String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id IN (";
sql += String.join(",", identifiers.stream().map(p -> "?").collect(Collectors.toList()));
sql += ")";
......@@ -150,6 +152,7 @@ public class GroupsDAO {
group.setName(resultSet.getString("name"));
group.setPath(resultSet.getString("path"));
group.setLeaf(resultSet.getBoolean("is_leaf"));
group.setLocked(resultSet.getBoolean("locked"));
groups.add(group);
}
return groups;
......@@ -158,7 +161,7 @@ public class GroupsDAO {
public Optional<GroupEntity> findGroupByParentAndName(String parentPath, String childName) {
String sql = "SELECT id, path, is_leaf from gms_group WHERE name = ? AND path ~ ?";
String sql = "SELECT id, path, is_leaf, locked from gms_group WHERE name = ? AND path ~ ?";
return jdbcTemplate.query(conn -> {
PreparedStatement ps = conn.prepareStatement(sql);
......@@ -172,6 +175,7 @@ public class GroupsDAO {
group.setName(childName);
group.setPath(resultSet.getString("path"));
group.setLeaf(resultSet.getBoolean("is_leaf"));
group.setLocked(resultSet.getBoolean("locked"));
return Optional.of(group);
}
return Optional.empty();
......@@ -191,9 +195,9 @@ public class GroupsDAO {
String sql;
if (hasSearchFilter) {
sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name";