@ -1,10 +1,10 @@
|
||||
CREATE TABLE event
|
||||
(
|
||||
event_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
group_id VARCHAR(36) NOT NULL,
|
||||
group_version INT NOT NULL,
|
||||
exec_id VARCHAR(50) NOT NULL,
|
||||
target_id VARCHAR(50),
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
event_payload VARCHAR(2500) NOT NULL
|
||||
group_id VARCHAR(36) NOT NULL,
|
||||
group_version INT NOT NULL,
|
||||
exec_id VARCHAR(32) NOT NULL,
|
||||
target_id VARCHAR(32),
|
||||
event_type VARCHAR(16) NOT NULL,
|
||||
event_payload JSON NOT NULL
|
||||
);
|
||||
|
10
mysql/db/schema-heroku.sql
Normal file
10
mysql/db/schema-heroku.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE event
|
||||
(
|
||||
event_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
group_id VARCHAR(36) NOT NULL,
|
||||
group_version INT NOT NULL,
|
||||
exec_id VARCHAR(32) NOT NULL,
|
||||
target_id VARCHAR(32),
|
||||
event_type VARCHAR(16) NOT NULL,
|
||||
event_payload TEXT NOT NULL
|
||||
);
|
@ -6,7 +6,7 @@ import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
public class FormatterConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
@ -9,6 +9,7 @@ import mops.gruppen2.domain.exception.IdMismatchException;
|
||||
import mops.gruppen2.domain.exception.UserAlreadyExistsException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.User;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@ -32,6 +33,11 @@ public class AddMemberEvent extends Event {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {
|
||||
cache.usersPut(target, group);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws UserAlreadyExistsException, GroupFullException {
|
||||
group.addMember(target, user);
|
||||
|
@ -6,6 +6,7 @@ import lombok.Value;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.BadArgumentException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
@ -23,6 +24,12 @@ public class CreateGroupEvent extends Event {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {
|
||||
cache.groupsPut(groupid, group);
|
||||
cache.linksPut(group.getLink(), group);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws BadArgumentException {
|
||||
group.setId(groupid);
|
||||
|
@ -5,6 +5,7 @@ import lombok.Value;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.NoAccessException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@ -17,6 +18,11 @@ public class DestroyGroupEvent extends Event {
|
||||
super(groupId, exec, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {
|
||||
cache.groupsRemove(group);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws NoAccessException {
|
||||
group.destroy(exec);
|
||||
|
@ -36,7 +36,7 @@ public abstract class Event {
|
||||
protected UUID groupid;
|
||||
|
||||
@JsonProperty("version")
|
||||
protected long version;
|
||||
protected long version; // Group-Version
|
||||
|
||||
@JsonProperty("exec")
|
||||
protected String exec;
|
||||
@ -68,9 +68,9 @@ public abstract class Event {
|
||||
}
|
||||
|
||||
checkGroupIdMatch(group.getId());
|
||||
updateCache(cache, group);
|
||||
group.updateVersion(version);
|
||||
applyEvent(group);
|
||||
updateCache(cache, group);
|
||||
}
|
||||
|
||||
private void checkGroupIdMatch(UUID groupid) throws IdMismatchException {
|
||||
@ -84,15 +84,7 @@ public abstract class Event {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCache(GroupCache cache, Group group) {
|
||||
if (this instanceof CreateGroupEvent) {
|
||||
cache.put(group);
|
||||
}
|
||||
|
||||
if (this instanceof DestroyGroupEvent) {
|
||||
cache.remove(group);
|
||||
}
|
||||
}
|
||||
protected abstract void updateCache(GroupCache cache, Group group);
|
||||
|
||||
protected abstract void applyEvent(Group group) throws EventException;
|
||||
|
||||
|
@ -6,6 +6,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.LastAdminException;
|
||||
import mops.gruppen2.domain.exception.UserNotFoundException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@ -21,6 +22,11 @@ public class KickMemberEvent extends Event {
|
||||
super(groupId, exec, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {
|
||||
cache.usersRemove(target, group);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws UserNotFoundException, LastAdminException {
|
||||
group.kickMember(target);
|
||||
|
@ -7,6 +7,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.NoAccessException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Description;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.UUID;
|
||||
@ -27,6 +28,9 @@ public class SetDescriptionEvent extends Event {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws NoAccessException {
|
||||
group.setDescription(exec, description);
|
||||
|
@ -7,6 +7,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.NoAccessException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Link;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.UUID;
|
||||
@ -24,6 +25,12 @@ public class SetInviteLinkEvent extends Event {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {
|
||||
cache.linksRemove(group.getLink());
|
||||
cache.linksPut(link.getValue(), group);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws NoAccessException {
|
||||
group.setLink(exec, link);
|
||||
|
@ -8,6 +8,7 @@ import mops.gruppen2.domain.exception.BadArgumentException;
|
||||
import mops.gruppen2.domain.exception.NoAccessException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Limit;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.UUID;
|
||||
@ -25,6 +26,9 @@ public class SetLimitEvent extends Event {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws BadArgumentException, NoAccessException {
|
||||
group.setLimit(exec, limit);
|
||||
|
@ -7,6 +7,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.NoAccessException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Parent;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.UUID;
|
||||
@ -24,6 +25,9 @@ public class SetParentEvent extends Event {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws NoAccessException {
|
||||
group.setParent(exec, parent);
|
||||
|
@ -7,6 +7,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.NoAccessException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Title;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.UUID;
|
||||
@ -27,6 +28,9 @@ public class SetTitleEvent extends Event {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws NoAccessException {
|
||||
group.setTitle(exec, title);
|
||||
|
@ -7,6 +7,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.EventException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.Type;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.UUID;
|
||||
@ -25,6 +26,12 @@ public class SetTypeEvent extends Event {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {
|
||||
cache.typesRemove(group);
|
||||
cache.typesPut(type, group);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws EventException {
|
||||
group.setType(exec, type);
|
||||
|
@ -8,6 +8,7 @@ import mops.gruppen2.domain.exception.LastAdminException;
|
||||
import mops.gruppen2.domain.exception.UserNotFoundException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.Role;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@ -27,6 +28,9 @@ public class UpdateRoleEvent extends Event {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateCache(GroupCache cache, Group group) {}
|
||||
|
||||
@Override
|
||||
protected void applyEvent(Group group) throws UserNotFoundException, LastAdminException {
|
||||
group.memberPutRole(target, role);
|
||||
|
@ -1,7 +1,9 @@
|
||||
package mops.gruppen2.domain.model.group;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.BadArgumentException;
|
||||
import mops.gruppen2.domain.exception.GroupFullException;
|
||||
@ -35,6 +37,7 @@ import java.util.stream.Collectors;
|
||||
* Muss beim Start gesetzt werden: groupid, meta
|
||||
*/
|
||||
@Log4j2
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class Group {
|
||||
|
||||
@ -55,9 +58,6 @@ public class Group {
|
||||
|
||||
private GroupOptions options = GroupOptions.DEFAULT();
|
||||
|
||||
//@ToString.Exclude
|
||||
//private LocalDateTime age;
|
||||
|
||||
// Inhalt
|
||||
private Title title = Title.EMPTY();
|
||||
|
||||
@ -112,6 +112,12 @@ public class Group {
|
||||
memberships.remove(target);
|
||||
}
|
||||
|
||||
public boolean memberHasRole(String target, Role role) {
|
||||
ValidationHelper.throwIfNoMember(this, target);
|
||||
|
||||
return memberships.get(target).getRole() == role;
|
||||
}
|
||||
|
||||
public void memberPutRole(String target, Role role) throws UserNotFoundException, LastAdminException {
|
||||
ValidationHelper.throwIfNoMember(this, target);
|
||||
if (role == Role.REGULAR) {
|
||||
@ -121,12 +127,6 @@ public class Group {
|
||||
memberships.put(target, memberships.get(target).setRole(role));
|
||||
}
|
||||
|
||||
public boolean memberHasRole(String target, Role role) {
|
||||
ValidationHelper.throwIfNoMember(this, target);
|
||||
|
||||
return memberships.get(target).getRole() == role;
|
||||
}
|
||||
|
||||
public boolean isMember(String target) {
|
||||
return memberships.containsKey(target);
|
||||
}
|
||||
|
@ -11,13 +11,13 @@ import java.time.LocalDateTime;
|
||||
@Log4j2
|
||||
@Value
|
||||
@ToString
|
||||
public class GroupMeta {
|
||||
class GroupMeta {
|
||||
|
||||
long version;
|
||||
String creator;
|
||||
LocalDateTime creationDate;
|
||||
|
||||
public GroupMeta setVersion(long version) throws IdMismatchException {
|
||||
GroupMeta setVersion(long version) throws IdMismatchException {
|
||||
if (this.version >= version) {
|
||||
throw new IdMismatchException("Die Gruppe ist bereits auf einem neueren Stand.");
|
||||
}
|
||||
@ -25,7 +25,7 @@ public class GroupMeta {
|
||||
return new GroupMeta(version, creator, creationDate);
|
||||
}
|
||||
|
||||
public GroupMeta setCreator(String userid) throws BadArgumentException {
|
||||
GroupMeta setCreator(String userid) throws BadArgumentException {
|
||||
if (creator != null) {
|
||||
throw new BadArgumentException("Gruppe hat schon einen Ersteller.");
|
||||
}
|
||||
@ -33,7 +33,7 @@ public class GroupMeta {
|
||||
return new GroupMeta(version, userid, creationDate);
|
||||
}
|
||||
|
||||
public GroupMeta setCreationDate(LocalDateTime date) throws BadArgumentException {
|
||||
GroupMeta setCreationDate(LocalDateTime date) throws BadArgumentException {
|
||||
if (creationDate != null) {
|
||||
throw new BadArgumentException("Gruppe hat schon ein Erstellungsdatum.");
|
||||
}
|
||||
@ -41,7 +41,7 @@ public class GroupMeta {
|
||||
return new GroupMeta(version, creator, date);
|
||||
}
|
||||
|
||||
public static GroupMeta EMPTY() {
|
||||
static GroupMeta EMPTY() {
|
||||
return new GroupMeta(0, null, null);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ public final class SortHelper {
|
||||
*
|
||||
* @param groups Die Liste von Gruppen die sortiert werden soll
|
||||
*/
|
||||
public static List<Group> sortByGroupType(List<Group> groups) {
|
||||
public static void sortByGroupType(List<Group> groups) {
|
||||
groups.sort((Group g1, Group g2) -> {
|
||||
if (g1.getType() == Type.LECTURE) {
|
||||
return -1;
|
||||
@ -22,10 +22,16 @@ public final class SortHelper {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (g1.getType() == Type.PUBLIC) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (g2.getType() == Type.PUBLIC) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
public static List<Membership> sortByMemberRole(List<Membership> memberships) {
|
||||
|
@ -3,23 +3,16 @@ package mops.gruppen2.domain.service;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.event.AddMemberEvent;
|
||||
import mops.gruppen2.domain.event.Event;
|
||||
import mops.gruppen2.domain.event.EventType;
|
||||
import mops.gruppen2.domain.exception.BadPayloadException;
|
||||
import mops.gruppen2.domain.service.helper.JsonHelper;
|
||||
import mops.gruppen2.persistance.EventRepository;
|
||||
import mops.gruppen2.persistance.dto.EventDTO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static mops.gruppen2.domain.event.EventType.DESTROYGROUP;
|
||||
import static mops.gruppen2.domain.service.helper.CommonHelper.eventTypesToString;
|
||||
|
||||
@Log4j2
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@ -46,20 +39,6 @@ public class EventStoreService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert alle Events aus der übergebenen Liste in der DB.
|
||||
*
|
||||
* @param events Liste an Events die gespeichert werden soll
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final void saveAll(List<Event>... events) {
|
||||
for (List<Event> eventlist : events) {
|
||||
for (Event event : eventlist) {
|
||||
eventStore.save(getDTOFromEvent(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//########################################### DTOs ###########################################
|
||||
|
||||
@ -110,82 +89,10 @@ public class EventStoreService {
|
||||
}
|
||||
|
||||
|
||||
// ######################################## QUERIES ##########################################
|
||||
|
||||
|
||||
/**
|
||||
* Liefert Gruppen-Ids von existierenden (ungelöschten) Gruppen, in welchen der User teilnimmt.
|
||||
*
|
||||
* <p>
|
||||
* Vorgang:
|
||||
* Finde für jede Gruppe das letzte Add- oder Kick-Event, welches den User betrifft
|
||||
* Finde für jede Gruppe das letzte Destroy-Event
|
||||
* Entferne alle alle Events von Gruppen, welche ein Destroy-Event haben
|
||||
* Gebe die Gruppen zurück, auf welche sich die Add-Events beziehen
|
||||
*
|
||||
* @return GruppenIds (UUID) als Liste
|
||||
*/
|
||||
public List<UUID> findExistingUserGroups(String userid) {
|
||||
List<Event> userEvents = findLatestEventsFromGroupsByUser(userid);
|
||||
List<UUID> deletedIds = findLatestEventsFromGroupsByType(DESTROYGROUP)
|
||||
.stream()
|
||||
.map(Event::getGroupid)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
userEvents.removeIf(event -> deletedIds.contains(event.getGroupid()));
|
||||
|
||||
return userEvents.stream()
|
||||
.filter(event -> event instanceof AddMemberEvent)
|
||||
.map(Event::getGroupid)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
// #################################### SIMPLE QUERIES #######################################
|
||||
|
||||
|
||||
/**
|
||||
* Ermittelt die Id zuletzt gespeicherten Events.
|
||||
*
|
||||
* @return Letzte EventId
|
||||
*/
|
||||
public long findMaxEventId() {
|
||||
try {
|
||||
return eventStore.findMaxEventId();
|
||||
} catch (NullPointerException e) {
|
||||
log.debug("Keine Events vorhanden!");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht zu jeder Gruppe das letzte Add- oder DeleteUserEvent heraus, welches den übergebenen User betrifft.
|
||||
*
|
||||
* @param userid User, zu welchem die Events gesucht werden
|
||||
*
|
||||
* @return Eine Liste von einem Add- oder DeleteUserEvent pro Gruppe
|
||||
*/
|
||||
private List<Event> findLatestEventsFromGroupsByUser(String userid) {
|
||||
return getEventsFromDTOs(eventStore.findLatestEventDTOsPartitionedByGroupTarget(userid));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sucht zu jeder Gruppe das letzte Event des/der übergebenen Typen heraus.
|
||||
*
|
||||
* @param types Eventtyp, nach welchem gesucht wird
|
||||
*
|
||||
* @return Eine Liste von einem Event pro Gruppe
|
||||
*/
|
||||
private List<Event> findLatestEventsFromGroupsByType(EventType... types) {
|
||||
return getEventsFromDTOs(eventStore.findLatestEventDTOsPartitionedByGroupByType(Arrays.asList(eventTypesToString(types))));
|
||||
}
|
||||
|
||||
public List<Event> findAllEvents() {
|
||||
return getEventsFromDTOs(eventStore.findAllEvents());
|
||||
}
|
||||
|
||||
public List<Event> findNewEvents(long version, long maxid) {
|
||||
return getEventsFromDTOs(eventStore.findNewEvents(version, maxid));
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import mops.gruppen2.domain.model.group.wrapper.Limit;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Link;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Parent;
|
||||
import mops.gruppen2.domain.model.group.wrapper.Title;
|
||||
import mops.gruppen2.domain.service.helper.ValidationHelper;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -139,7 +138,7 @@ public class GroupService {
|
||||
Event event = new CreateGroupEvent(groupid,
|
||||
exec,
|
||||
date);
|
||||
Group group = new Group();
|
||||
Group group = Group.EMPTY();
|
||||
applyAndSave(group, event);
|
||||
|
||||
return group;
|
||||
@ -171,7 +170,7 @@ public class GroupService {
|
||||
public void kickMember(Group group, String exec, String target) {
|
||||
applyAndSave(group, new KickMemberEvent(group.getId(), exec, target));
|
||||
|
||||
if (ValidationHelper.checkIfGroupEmpty(group)) {
|
||||
if (group.isEmpty()) {
|
||||
deleteGroup(group, exec);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,11 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.EventException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.SortHelper;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -29,10 +31,12 @@ public class SearchService {
|
||||
* @throws EventException Projektionsfehler
|
||||
*/
|
||||
//TODO: search in lectures
|
||||
public List<Group> searchPublicGroups(String search, String principal) {
|
||||
List<Group> groups = groupCache.publics();
|
||||
|
||||
public List<Group> search(String search, String principal) {
|
||||
List<Group> groups = new ArrayList<>();
|
||||
groups.addAll(groupCache.publics());
|
||||
groups.addAll(groupCache.lectures());
|
||||
groups = removeUserGroups(groups, principal);
|
||||
SortHelper.sortByGroupType(groups);
|
||||
|
||||
if (search.isEmpty()) {
|
||||
return groups;
|
||||
@ -50,5 +54,4 @@ public class SearchService {
|
||||
.filter(group -> !group.isMember(principal))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,17 +3,13 @@ package mops.gruppen2.domain.service.helper;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.infrastructure.api.GroupRequestWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
//TODO: sinnvolles format
|
||||
@Log4j2
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class APIHelper {
|
||||
|
||||
public static GroupRequestWrapper wrap(long status, List<Group> groupList) {
|
||||
/*public static GroupRequestWrapper wrap(long status, List<Group> groupList) {
|
||||
return new GroupRequestWrapper(status, groupList);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
@ -3,39 +3,14 @@ package mops.gruppen2.domain.service.helper;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.event.EventType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Log4j2
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class CommonHelper {
|
||||
|
||||
public static String[] eventTypesToString(EventType... types) {
|
||||
String[] stringtypes = new String[types.length];
|
||||
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
stringtypes[i] = types[i].toString();
|
||||
}
|
||||
|
||||
return stringtypes;
|
||||
}
|
||||
|
||||
public static List<String> uuidsToString(List<UUID> ids) {
|
||||
return ids.stream()
|
||||
.map(UUID::toString)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static boolean uuidIsEmpty(UUID uuid) {
|
||||
return "00000000-0000-0000-0000-000000000000".equals(uuid.toString());
|
||||
}
|
||||
|
||||
public static List<UUID> stringsToUUID(List<String> groupids) {
|
||||
return groupids.stream()
|
||||
.map(UUID::fromString)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ public final class ProjectionHelper {
|
||||
*/
|
||||
private static Group getOrCreateGroup(Map<UUID, Group> groups, UUID groupId) {
|
||||
if (!groups.containsKey(groupId)) {
|
||||
groups.put(groupId, new Group());
|
||||
groups.put(groupId, Group.EMPTY());
|
||||
}
|
||||
|
||||
return groups.get(groupId);
|
||||
|
@ -17,36 +17,15 @@ import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class ValidationHelper {
|
||||
|
||||
/**
|
||||
* Überprüft, ob ein User in einer Gruppe teilnimmt.
|
||||
*/
|
||||
public static boolean checkIfMember(Group group, String userid) {
|
||||
return group.isMember(userid);
|
||||
}
|
||||
|
||||
public static boolean checkIfLastMember(Group group, String userid) {
|
||||
return checkIfMember(group, userid) && group.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob eine Gruppe voll ist.
|
||||
*/
|
||||
public static boolean checkIfGroupFull(Group group) {
|
||||
return group.isFull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob eine Gruppe leer ist.
|
||||
*/
|
||||
public static boolean checkIfGroupEmpty(Group group) {
|
||||
return group.isEmpty();
|
||||
return group.isMember(userid) && group.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft, ob ein User in einer Gruppe Admin ist.
|
||||
*/
|
||||
public static boolean checkIfAdmin(Group group, String userid) {
|
||||
if (checkIfMember(group, userid)) {
|
||||
if (group.isMember(userid)) {
|
||||
return group.isAdmin(userid);
|
||||
}
|
||||
return false;
|
||||
@ -61,14 +40,14 @@ public final class ValidationHelper {
|
||||
|
||||
|
||||
public static void throwIfMember(Group group, String userid) throws UserAlreadyExistsException {
|
||||
if (checkIfMember(group, userid)) {
|
||||
if (group.isMember(userid)) {
|
||||
log.error("Benutzer {} ist schon in Gruppe {}", userid, group);
|
||||
throw new UserAlreadyExistsException(userid);
|
||||
}
|
||||
}
|
||||
|
||||
public static void throwIfNoMember(Group group, String userid) throws UserNotFoundException {
|
||||
if (!checkIfMember(group, userid)) {
|
||||
if (!group.isMember(userid)) {
|
||||
log.error("Benutzer {} ist nicht in Gruppe {}!", userid, group);
|
||||
throw new UserNotFoundException(userid);
|
||||
}
|
||||
@ -91,7 +70,7 @@ public final class ValidationHelper {
|
||||
}
|
||||
|
||||
public static void throwIfGroupFull(Group group) throws GroupFullException {
|
||||
if (checkIfGroupFull(group)) {
|
||||
if (group.isFull()) {
|
||||
log.error("Die Gruppe {} ist voll!", group);
|
||||
throw new GroupFullException(group.getId().toString());
|
||||
}
|
||||
|
@ -1,19 +1,24 @@
|
||||
package mops.gruppen2.infrastructure;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.domain.exception.GroupNotFoundException;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.Type;
|
||||
import mops.gruppen2.domain.service.EventStoreService;
|
||||
import mops.gruppen2.domain.service.helper.ProjectionHelper;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Log4j2
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Scope("singleton")
|
||||
@ -22,21 +27,21 @@ public class GroupCache {
|
||||
private final EventStoreService eventStoreService;
|
||||
|
||||
private final Map<UUID, Group> groups = new HashMap<>();
|
||||
private final Map<String, Group> links = new HashMap<>();
|
||||
private final Map<String, List<Group>> users = new HashMap<>();
|
||||
private final Map<Type, List<Group>> types = new EnumMap<>(Type.class);
|
||||
|
||||
public void init() {
|
||||
long maxid = eventStoreService.findMaxEventId();
|
||||
ProjectionHelper.project(groups, eventStoreService.findNewEvents(0, maxid), this);
|
||||
|
||||
// ######################################## CACHE ###########################################
|
||||
|
||||
|
||||
void init() {
|
||||
ProjectionHelper.project(groups, eventStoreService.findAllEvents(), this);
|
||||
}
|
||||
|
||||
public void put(Group group) {
|
||||
groups.put(group.getId(), group);
|
||||
}
|
||||
|
||||
public void remove(Group group) {
|
||||
groups.remove(group.getId());
|
||||
}
|
||||
// ########################################### GETTERS #######################################
|
||||
|
||||
// Getters
|
||||
|
||||
public Group group(UUID groupid) {
|
||||
if (!groups.containsKey(groupid)) {
|
||||
@ -47,33 +52,96 @@ public class GroupCache {
|
||||
}
|
||||
|
||||
public Group group(String link) {
|
||||
return groups.values().stream()
|
||||
.filter(group -> group.getLink().equals(link))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new GroupNotFoundException("Link nicht im Cache."));
|
||||
if (!links.containsKey(link)) {
|
||||
throw new GroupNotFoundException("Link ist nicht im Cache.");
|
||||
}
|
||||
|
||||
return links.get(link);
|
||||
}
|
||||
|
||||
public List<Group> userGroups(String userid) {
|
||||
return groups.values().stream()
|
||||
.filter(group -> group.isMember(userid))
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
if (!users.containsKey(userid)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(users.get(userid));
|
||||
}
|
||||
|
||||
public List<Group> publics() {
|
||||
return groups.values().stream()
|
||||
.filter(Group::isPublic)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
if (!types.containsKey(Type.PUBLIC)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(types.get(Type.PUBLIC));
|
||||
}
|
||||
|
||||
public List<Group> privates() {
|
||||
return groups.values().stream()
|
||||
.filter(Group::isPrivate)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
if (!types.containsKey(Type.PRIVATE)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(types.get(Type.PRIVATE));
|
||||
}
|
||||
|
||||
public List<Group> lectures() {
|
||||
return groups.values().stream()
|
||||
.filter(Group::isLecture)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
if (!types.containsKey(Type.LECTURE)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(types.get(Type.LECTURE));
|
||||
}
|
||||
|
||||
|
||||
// ######################################## SETTERS ##########################################
|
||||
|
||||
|
||||
public void usersPut(String userid, Group group) {
|
||||
if (!users.containsKey(userid)) {
|
||||
users.put(userid, new ArrayList<>());
|
||||
log.debug("Ein User wurde dem Cache hinzugefügt.");
|
||||
}
|
||||
|
||||
users.get(userid).add(group);
|
||||
}
|
||||
|
||||
public void usersRemove(String target, Group group) {
|
||||
if (!users.containsKey(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
users.get(target).remove(group);
|
||||
}
|
||||
|
||||
public void groupsPut(UUID groupid, Group group) {
|
||||
groups.put(groupid, group);
|
||||
}
|
||||
|
||||
public void groupsRemove(Group group) {
|
||||
groups.remove(group.getId());
|
||||
}
|
||||
|
||||
public void linksPut(String link, Group group) {
|
||||
links.put(link, group);
|
||||
}
|
||||
|
||||
public void linksRemove(String link) {
|
||||
links.remove(link);
|
||||
}
|
||||
|
||||
public void typesPut(Type type, Group group) {
|
||||
if (!types.containsKey(type)) {
|
||||
types.put(type, new ArrayList<>());
|
||||
log.debug("Ein Typ wurde dem Cache hinzugefügt.");
|
||||
}
|
||||
|
||||
types.get(type).add(group);
|
||||
}
|
||||
|
||||
public void typesRemove(Group group) {
|
||||
if (!types.containsKey(group.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
types.get(group.getType()).remove(group);
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,6 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@ControllerAdvice
|
||||
public class ModelAttributeControllerAdvice {
|
||||
|
||||
private final GroupCache groupCache;
|
||||
|
||||
// Add modelAttributes before each @RequestMapping
|
||||
@ModelAttribute
|
||||
public void modelAttributes(KeycloakAuthenticationToken token,
|
||||
|
@ -1,8 +1,6 @@
|
||||
package mops.gruppen2.infrastructure.api;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -10,9 +8,8 @@ import java.util.List;
|
||||
* Kombiniert den Status und die Gruppenliste zur ausgabe über die API.
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class GroupRequestWrapper {
|
||||
|
||||
private final long status;
|
||||
private final List<Group> groupList;
|
||||
private final long version;
|
||||
private final List<GroupWrapper> groups;
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
package mops.gruppen2.infrastructure.api;
|
||||
|
||||
public class GroupWrapper {
|
||||
}
|
@ -1,21 +1,13 @@
|
||||
package mops.gruppen2.infrastructure.controller;
|
||||
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
|
||||
import mops.gruppen2.domain.service.EventStoreService;
|
||||
import mops.gruppen2.domain.service.helper.CommonHelper;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Api zum Datenabgleich.
|
||||
*/
|
||||
@ -49,14 +41,14 @@ public class APIController {
|
||||
/**
|
||||
* Gibt die Gruppen-IDs von Gruppen, in welchen der übergebene Nutzer teilnimmt, zurück.
|
||||
*/
|
||||
@GetMapping("/usergroups/{id}")
|
||||
/*@GetMapping("/usergroups/{id}")
|
||||
@Secured("ROLE_api_user")
|
||||
@ApiOperation("Gibt Gruppen zurück, in welchen ein Nutzer teilnimmt")
|
||||
public List<String> getApiUserGroups(@ApiParam("Nutzer-Id")
|
||||
@PathVariable("id") String userId) {
|
||||
|
||||
return CommonHelper.uuidsToString(eventStoreService.findExistingUserGroups(userId));
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Konstruiert eine einzelne, vollständige Gruppe.
|
||||
|
@ -57,7 +57,7 @@ public class GroupDetailsController {
|
||||
model.addAttribute("parent", parent);
|
||||
|
||||
// Detailseite für nicht-Mitglieder
|
||||
if (!ValidationHelper.checkIfMember(group, principal)) {
|
||||
if (!group.isMember(principal)) {
|
||||
return "preview";
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ public class GroupDetailsController {
|
||||
String principal = token.getName();
|
||||
Group group = groupCache.group(UUID.fromString(groupId));
|
||||
|
||||
if (ValidationHelper.checkIfMember(group, principal)) {
|
||||
if (group.isMember(principal)) {
|
||||
return "redirect:/gruppen2/details/" + groupId;
|
||||
}
|
||||
|
||||
|
@ -28,11 +28,6 @@ public class GruppenfindungController {
|
||||
return "redirect:/gruppen2";
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login() {
|
||||
return "redirect:/gruppen2";
|
||||
}
|
||||
|
||||
@TraceMethodCall
|
||||
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
|
||||
@GetMapping("/gruppen2")
|
||||
|
@ -6,7 +6,6 @@ import mops.gruppen2.aspect.annotation.TraceMethodCalls;
|
||||
import mops.gruppen2.domain.model.group.Group;
|
||||
import mops.gruppen2.domain.model.group.Type;
|
||||
import mops.gruppen2.domain.service.SearchService;
|
||||
import mops.gruppen2.domain.service.helper.ValidationHelper;
|
||||
import mops.gruppen2.infrastructure.GroupCache;
|
||||
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@ -48,7 +47,7 @@ public class SearchAndInviteController {
|
||||
@RequestParam("string") String search) {
|
||||
|
||||
String principal = token.getName();
|
||||
List<Group> groups = searchService.searchPublicGroups(search, principal);
|
||||
List<Group> groups = searchService.search(search, principal);
|
||||
|
||||
model.addAttribute("groups", groups);
|
||||
|
||||
@ -72,7 +71,7 @@ public class SearchAndInviteController {
|
||||
}
|
||||
|
||||
// Bereits Mitglied
|
||||
if (ValidationHelper.checkIfMember(group, principal)) {
|
||||
if (group.isMember(principal)) {
|
||||
return "redirect:/gruppen2/details/" + group.getId();
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ package mops.gruppen2.persistance;
|
||||
import mops.gruppen2.persistance.dto.EventDTO;
|
||||
import org.springframework.data.jdbc.repository.query.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -15,37 +14,6 @@ public interface EventRepository extends CrudRepository<EventDTO, Long> {
|
||||
// ####################################### EVENT DTOs ########################################
|
||||
|
||||
|
||||
@Query("SELECT * FROM event WHERE event_id > :version AND event_id <= :max")
|
||||
List<EventDTO> findNewEvents(@Param("version") long version,
|
||||
@Param("max") long maxid);
|
||||
|
||||
@Query("SELECT * FROM event")
|
||||
List<EventDTO> findAllEvents();
|
||||
|
||||
|
||||
// ################################ LATEST EVENT DTOs ########################################
|
||||
|
||||
|
||||
@Query("WITH ranked_events AS ("
|
||||
+ "SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY event_id DESC) AS rn"
|
||||
+ " FROM event"
|
||||
+ " WHERE target_id = :userId AND event_type IN ('ADDMEMBER', 'KICKMEMBER')"
|
||||
+ ")"
|
||||
+ "SELECT * FROM ranked_events WHERE rn = 1;")
|
||||
List<EventDTO> findLatestEventDTOsPartitionedByGroupTarget(@Param("userId") String target);
|
||||
|
||||
@Query("WITH ranked_events AS ("
|
||||
+ "SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY event_id DESC) AS rn"
|
||||
+ " FROM event"
|
||||
+ " WHERE event_type IN (:types)"
|
||||
+ ")"
|
||||
+ "SELECT * FROM ranked_events WHERE rn = 1;")
|
||||
List<EventDTO> findLatestEventDTOsPartitionedByGroupByType(@Param("types") List<String> types);
|
||||
|
||||
|
||||
// ######################################### COUNT ###########################################
|
||||
|
||||
|
||||
@Query("SELECT MAX(event_id) FROM event")
|
||||
Long findMaxEventId();
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ import org.springframework.data.relational.core.mapping.Table;
|
||||
public class EventDTO {
|
||||
|
||||
@Id
|
||||
Long event_id;
|
||||
Long event_id; // Cache-Version
|
||||
|
||||
String group_id;
|
||||
long group_version;
|
||||
long group_version; // Group-Version
|
||||
|
||||
String exec_id;
|
||||
String target_id;
|
||||
|
@ -10,7 +10,6 @@ spring.datasource.url = mysql://b4b665d39d0670:cc933ff7@eu
|
||||
spring.datasource.username = b4b665d39d0670
|
||||
spring.datasource.password = cc933ff7
|
||||
|
||||
|
||||
# Misc
|
||||
management.endpoints.web.exposure.include = info,health
|
||||
server.error.include-stacktrace = always
|
||||
|
@ -9,7 +9,7 @@ function enable(id) {
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
const copyText = document.getElementById("groupLink");
|
||||
const copyText = document.getElementById("linkview");
|
||||
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999);
|
||||
|
@ -34,7 +34,7 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Einladungslink:</span>
|
||||
</div>
|
||||
<input class="form-control" readonly th:value="${link}" type="text">
|
||||
<input class="form-control" id="linkview" readonly th:value="${link}" type="text">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
onclick="copyLink()">Link kopieren
|
||||
|
@ -7,7 +7,7 @@
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
|
||||
<script src="https://kit.fontawesome.com/22c0caaa8a.js" crossorigin="anonymous"></script>
|
||||
<!--<script src="https://kit.fontawesome.com/22c0caaa8a.js" crossorigin="anonymous"></script>-->
|
||||
|
||||
<script type="text/javascript" th:src="@{/js/script.js}"></script>
|
||||
|
||||
|
Reference in New Issue
Block a user