diff --git a/src/main/angular/src/app/api/User/UserPrivate.ts b/src/main/angular/src/app/api/User/UserPrivate.ts index 507cd0a..7c44d61 100644 --- a/src/main/angular/src/app/api/User/UserPrivate.ts +++ b/src/main/angular/src/app/api/User/UserPrivate.ts @@ -1,24 +1,27 @@ import {validateBoolean, validateDate, validateString} from "../common/validators"; +import {UserPublic} from "./UserPublic"; -export class UserPrivate { +export class UserPrivate extends UserPublic { constructor( readonly privateUuid: string, - readonly publicUuid: string, + publicUuid: string, readonly created: Date, - readonly name: string, + name: string, readonly password: boolean, + admin: boolean, ) { - // - + super(publicUuid, name, admin); } - static fromJson(json: any): UserPrivate { + static override fromJson(json: any): UserPrivate { return new UserPrivate( validateString(json['privateUuid']), validateString(json['publicUuid']), validateDate(json['created']), validateString(json['name']), validateBoolean(json['password']), + validateBoolean(json['admin']), ); } @@ -29,8 +32,4 @@ export class UserPrivate { return UserPrivate.fromJson(json); } - static samePrivateUuid(a: UserPrivate, b: UserPrivate): boolean { - return a.privateUuid === b.privateUuid; - } - } diff --git a/src/main/angular/src/app/api/User/UserPublic.ts b/src/main/angular/src/app/api/User/UserPublic.ts index 1566cdd..4b26d57 100644 --- a/src/main/angular/src/app/api/User/UserPublic.ts +++ b/src/main/angular/src/app/api/User/UserPublic.ts @@ -1,10 +1,12 @@ -import {validateString} from "../common/validators"; +import {validateBoolean, validateString} from "../common/validators"; +import {UserPrivate} from "./UserPrivate"; export class UserPublic { constructor( readonly publicUuid: string, readonly name: string, + readonly admin: boolean, ) { // - } @@ -13,6 +15,7 @@ export class UserPublic { return new UserPublic( validateString(json['publicUuid']), validateString(json['name']), + validateBoolean(json['admin']), ); } @@ -20,4 +23,8 @@ export class UserPublic { return a.name.localeCompare(b.name); } + equals(user: UserPublic | UserPrivate | null) { + return user !== null && this.publicUuid === user.publicUuid; + } + } diff --git a/src/main/angular/src/app/api/User/user.service.ts b/src/main/angular/src/app/api/User/user.service.ts index dcd79c0..64f6435 100644 --- a/src/main/angular/src/app/api/User/user.service.ts +++ b/src/main/angular/src/app/api/User/user.service.ts @@ -85,7 +85,7 @@ export class UserService { } iOwn(group: Group): boolean { - return this.user?.publicUuid === group.owner.publicUuid; + return group.owner.equals(this.user); } private setUser(user: UserPrivate | null) { @@ -99,4 +99,8 @@ export class UserService { this.subject.next(user); } + iAm(user: UserPublic | UserPrivate | null) { + return this.user !== null && this.user.equals(user); + } + } diff --git a/src/main/angular/src/app/api/group/Group.ts b/src/main/angular/src/app/api/group/Group.ts index c6158af..0a01aa1 100644 --- a/src/main/angular/src/app/api/group/Group.ts +++ b/src/main/angular/src/app/api/group/Group.ts @@ -1,5 +1,6 @@ import {UserPublic} from "../User/UserPublic"; import {validateBoolean, validateDate, validateList, validateString} from "../common/validators"; +import {UserPrivate} from "../User/UserPrivate"; export class Group { @@ -10,6 +11,7 @@ export class Group { readonly title: string, readonly password: string, readonly users: UserPublic[], + readonly banned: UserPublic[], readonly initial: boolean, ) { // - @@ -27,6 +29,7 @@ export class Group { validateString(json['title']), validateString(json['password']), validateList(json['users'], UserPublic.fromJson), + validateList(json['banned'], UserPublic.fromJson), validateBoolean(json['initial']), ); } @@ -52,5 +55,13 @@ export class Group { return a.created.getTime() - b.created.getTime(); } + isOwnedBy(user: UserPublic | UserPrivate | null) { + return user !== null && user.equals(this.owner); + } + + bannedByName() { + return this.banned.sort(UserPublic.compareName); + } + } diff --git a/src/main/angular/src/app/api/group/group.service.ts b/src/main/angular/src/app/api/group/group.service.ts index b240b65..3216def 100644 --- a/src/main/angular/src/app/api/group/group.service.ts +++ b/src/main/angular/src/app/api/group/group.service.ts @@ -6,6 +6,11 @@ import {UserService} from "../User/user.service"; import {validateBoolean} from "../common/validators"; import {Injectable} from "@angular/core"; import {Subscribed} from "../Subscribed"; +import {UserPublic} from "../User/UserPublic"; +import {GroupUserRequest} from "./requests/GroupUserRequest"; +import {GroupChangeTitleRequest} from "./requests/GroupChangeTitleRequest"; +import {GroupChangePasswordRequest} from "./requests/GroupChangePasswordRequest"; +import {GroupJoinRequest} from "./requests/GroupJoinRequest"; @Injectable({ providedIn: 'root' @@ -41,26 +46,17 @@ export class GroupService { } changeTitle(group: Group, title: string, next?: Next): void { - const data = { - uuid: group.uuid, - title: title, - }; + const data = new GroupChangeTitleRequest(group, title); this.api.postSingle(['Group', 'changeTitle'], data, Group.fromJson, next); } changePassword(group: Group, password: string, next?: Next): void { - const data = { - uuid: group.uuid, - password: password, - }; + const data = new GroupChangePasswordRequest(group, password); this.api.postSingle(['Group', 'changePassword'], data, Group.fromJson, next); } - join(uuid: string, password: string, next: Next): void { - const data = { - uuid: uuid, - password: password, - }; + join(groupUuid: string, password: string, next: Next): void { + const data = new GroupJoinRequest(groupUuid, password); this.api.postSingle(['Group', 'join'], data, Group.fromJson, next); } @@ -84,5 +80,17 @@ export class GroupService { this.api.postNone(['Group', 'delete'], group.uuid, next); } + kick(group: Group, user: UserPublic, next?: Next): void { + this.api.postNone(['Group', 'kick'], new GroupUserRequest(group, user), next); + } + + ban(group: Group, user: UserPublic, next?: Next): void { + this.api.postNone(['Group', 'ban'], new GroupUserRequest(group, user), next); + } + + unban(group: Group, user: UserPublic, next?: Next): void { + this.api.postNone(['Group', 'unban'], new GroupUserRequest(group, user), next); + } + } diff --git a/src/main/angular/src/app/api/group/requests/GroupChangePasswordRequest.ts b/src/main/angular/src/app/api/group/requests/GroupChangePasswordRequest.ts new file mode 100644 index 0000000..260bf8b --- /dev/null +++ b/src/main/angular/src/app/api/group/requests/GroupChangePasswordRequest.ts @@ -0,0 +1,14 @@ +import {Group} from "../Group"; + +export class GroupChangePasswordRequest { + + readonly groupUuid: string; + + readonly password: string; + + constructor(group: Group, password: string) { + this.groupUuid = group.uuid; + this.password = password; + } + +} diff --git a/src/main/angular/src/app/api/group/requests/GroupChangeTitleRequest.ts b/src/main/angular/src/app/api/group/requests/GroupChangeTitleRequest.ts new file mode 100644 index 0000000..26dfa2f --- /dev/null +++ b/src/main/angular/src/app/api/group/requests/GroupChangeTitleRequest.ts @@ -0,0 +1,14 @@ +import {Group} from "../Group"; + +export class GroupChangeTitleRequest { + + readonly groupUuid: string; + + readonly title: string; + + constructor(group: Group, title: string) { + this.groupUuid = group.uuid; + this.title = title; + } + +} diff --git a/src/main/angular/src/app/api/group/requests/GroupJoinRequest.ts b/src/main/angular/src/app/api/group/requests/GroupJoinRequest.ts new file mode 100644 index 0000000..58b3e27 --- /dev/null +++ b/src/main/angular/src/app/api/group/requests/GroupJoinRequest.ts @@ -0,0 +1,12 @@ +export class GroupJoinRequest { + + readonly groupUuid: string; + + readonly password: string; + + constructor(groupUuid: string, password: string) { + this.groupUuid = groupUuid; + this.password = password; + } + +} diff --git a/src/main/angular/src/app/api/group/requests/GroupUserRequest.ts b/src/main/angular/src/app/api/group/requests/GroupUserRequest.ts new file mode 100644 index 0000000..f4c536b --- /dev/null +++ b/src/main/angular/src/app/api/group/requests/GroupUserRequest.ts @@ -0,0 +1,15 @@ +import {Group} from "../Group"; +import {UserPublic} from "../../User/UserPublic"; + +export class GroupUserRequest { + + readonly groupUuid: string; + + readonly userPublicUuid: string; + + constructor(group: Group, user: UserPublic) { + this.groupUuid = group.uuid; + this.userPublicUuid = user.publicUuid; + } + +} diff --git a/src/main/angular/src/app/api/tools/Numbers/Numbers.ts b/src/main/angular/src/app/api/tools/Numbers/Numbers.ts index 5fa9f89..5c82fd6 100644 --- a/src/main/angular/src/app/api/tools/Numbers/Numbers.ts +++ b/src/main/angular/src/app/api/tools/Numbers/Numbers.ts @@ -1,5 +1,5 @@ import {Group} from "../../group/Group"; -import {validateDate, validateNumberOrNull, validateString} from "../../common/validators"; +import {validateDate, validateDateOrNull, validateNumberOrNull, validateString} from "../../common/validators"; export class Numbers { @@ -7,6 +7,7 @@ export class Numbers { readonly uuid: string, readonly group: Group, readonly date: Date, + readonly read: Date | null, readonly number: number | null, ) { // - @@ -17,6 +18,7 @@ export class Numbers { validateString(json['uuid']), Group.fromJson(json['group']), validateDate(json['date']), + validateDateOrNull(json['read']), validateNumberOrNull(json['number']), ); } diff --git a/src/main/angular/src/app/api/tools/Numbers/numbers.service.ts b/src/main/angular/src/app/api/tools/Numbers/numbers.service.ts index 9d97fc6..f7cb449 100644 --- a/src/main/angular/src/app/api/tools/Numbers/numbers.service.ts +++ b/src/main/angular/src/app/api/tools/Numbers/numbers.service.ts @@ -30,8 +30,8 @@ export class NumbersService { this.api.getPage(['Numbers', 'page', groupUuid, page, pageSize], Numbers.fromJson, next); } - byUuid(numbersUuid: string, next: Next): void { - this.api.postSingle(['Numbers', 'byUuid'], numbersUuid, Numbers.fromJson, next); + fetchAndMarkAsRead(numbersUuid: string, next: Next): void { + this.api.postSingle(['Numbers', 'fetchAndMarkAsRead'], numbersUuid, Numbers.fromJson, next); } canAccess(numbersUuid: string, next: Next): void { @@ -42,4 +42,8 @@ export class NumbersService { this.api.postSingle(['Numbers', 'getGroupUuid'], numbersUuid, validateString, next); } + goto(numbers: Numbers) { + this.router.navigate(['Numbers', numbers.uuid]); + } + } diff --git a/src/main/angular/src/app/pages/group/group/group.component.html b/src/main/angular/src/app/pages/group/group/group.component.html index 37d7d9c..b3f1060 100644 --- a/src/main/angular/src/app/pages/group/group/group.component.html +++ b/src/main/angular/src/app/pages/group/group/group.component.html @@ -1,6 +1,6 @@
-
+
Passwort @@ -19,6 +19,7 @@ Gruppe
+ @@ -38,13 +39,66 @@ - - - -
Erstellt
Teilnehmer -
{{ user.name }}
-
+ +
+
+ Gruppe löschen +
+
+ +
+
+
+ +
+
+
+ Mitglieder +
+
+
+ + + + + +
{{ user.name }} + + Admin + +
+ +
Verbannen
+
Entfernen
+
+
+
+
+
+
+
+ +
+
+
+ Verbannt +
+
+
+ + + + + +
{{ user.name }} +
+ +
Aufheben
+
+
+
+
@@ -55,28 +109,17 @@ Nummern
+
+
+ Nächste Runde
+
- +
{{ numbers.date | relative:now }} {{ numbers.number || '-' }}
- -
-
- - -
-
-
- Löschen -
-
-
diff --git a/src/main/angular/src/app/pages/group/group/group.component.less b/src/main/angular/src/app/pages/group/group/group.component.less index 5426a67..9198bdd 100644 --- a/src/main/angular/src/app/pages/group/group/group.component.less +++ b/src/main/angular/src/app/pages/group/group/group.component.less @@ -3,3 +3,23 @@ th { text-align: left; } + +.read { + color: gray; +} + +.unread { + background-color: lightskyblue; +} + +.buttonRemove { + color: orange; +} + +.buttonBan { + color: red; +} + +.buttonUnban { + color: green; +} diff --git a/src/main/angular/src/app/pages/group/group/group.component.ts b/src/main/angular/src/app/pages/group/group/group.component.ts index fa24ef5..873c67d 100644 --- a/src/main/angular/src/app/pages/group/group/group.component.ts +++ b/src/main/angular/src/app/pages/group/group/group.component.ts @@ -94,8 +94,10 @@ export class GroupComponent implements OnInit, OnDestroy { } } - protected delete(group: Group): void { - this.groupService.delete(group, () => this.groupService.gotoGroups()); + protected groupDelete(group: Group): void { + if (confirm("Gruppe \"" + group.title + "\" wirklich löschen?")) { + this.groupService.delete(group, () => this.groupService.gotoGroups()); + } } } diff --git a/src/main/angular/src/app/pages/group/groups/groups.component.html b/src/main/angular/src/app/pages/group/groups/groups.component.html index b215964..820d2c5 100644 --- a/src/main/angular/src/app/pages/group/groups/groups.component.html +++ b/src/main/angular/src/app/pages/group/groups/groups.component.html @@ -11,7 +11,9 @@
- +
+
+ Neue Gruppen erstellen
+
diff --git a/src/main/angular/src/app/pages/group/groups/groups.component.ts b/src/main/angular/src/app/pages/group/groups/groups.component.ts index 6562c95..0bb58c0 100644 --- a/src/main/angular/src/app/pages/group/groups/groups.component.ts +++ b/src/main/angular/src/app/pages/group/groups/groups.component.ts @@ -5,13 +5,15 @@ import {NgIf} from "@angular/common"; import {GroupService} from "../../../api/group/group.service"; import {Group} from "../../../api/group/Group"; import {Subscription} from "rxjs"; +import {ReactiveFormsModule} from "@angular/forms"; @Component({ selector: 'app-groups', standalone: true, imports: [ GroupListComponent, - NgIf + NgIf, + ReactiveFormsModule ], templateUrl: './groups.component.html', styleUrl: './groups.component.less' diff --git a/src/main/angular/src/app/pages/tools/numbers/numbers.component.html b/src/main/angular/src/app/pages/tools/numbers/numbers.component.html index 504ed6d..26fc34f 100644 --- a/src/main/angular/src/app/pages/tools/numbers/numbers.component.html +++ b/src/main/angular/src/app/pages/tools/numbers/numbers.component.html @@ -5,16 +5,18 @@ [class.date_old]="now.getTime() - numbers.date.getTime() >= 5 * 60 * 1000" > - -
{{ numbers.date | relative:now }}
-
+
{{ numbers.number || '-' }}
+
+
+ Nächste Runde +
+
+
diff --git a/src/main/angular/src/app/pages/tools/numbers/numbers.component.ts b/src/main/angular/src/app/pages/tools/numbers/numbers.component.ts index 9f5c89b..9e86b92 100644 --- a/src/main/angular/src/app/pages/tools/numbers/numbers.component.ts +++ b/src/main/angular/src/app/pages/tools/numbers/numbers.component.ts @@ -1,4 +1,4 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Component, HostListener, OnDestroy, OnInit} from '@angular/core'; import {Numbers} from "../../../api/tools/Numbers/Numbers"; import {ActivatedRoute} from "@angular/router"; import {NumbersService} from "../../../api/tools/Numbers/numbers.service"; @@ -46,7 +46,7 @@ export class NumbersComponent implements OnInit, OnDestroy { if (uuid) { this.numbersService.canAccess(uuid, granted => { if (granted) { - this.numbersService.byUuid(uuid, numbers => this.numbers = numbers); + this.numbersService.fetchAndMarkAsRead(uuid, numbers => this.numbers = numbers); } else { this.numbersService.getGroupUuid(uuid, groupUuid => this.groupService.goto(groupUuid)); } @@ -64,4 +64,11 @@ export class NumbersComponent implements OnInit, OnDestroy { } } + @HostListener('window:keydown.escape') + protected gotoGroup() { + if (this.numbers?.group) { + this.groupService.goto(this.numbers.group.uuid); + } + } + } diff --git a/src/main/angular/src/app/shared/password/password.component.html b/src/main/angular/src/app/shared/password/password.component.html index 02ef367..6c7f814 100644 --- a/src/main/angular/src/app/shared/password/password.component.html +++ b/src/main/angular/src/app/shared/password/password.component.html @@ -1,5 +1,7 @@

Passwort

- +
+
Beitreten
+
diff --git a/src/main/angular/src/common.less b/src/main/angular/src/common.less index f29d435..948b194 100644 --- a/src/main/angular/src/common.less +++ b/src/main/angular/src/common.less @@ -1,2 +1,52 @@ @import "./tile.less"; @import "./user.less"; + +.buttons { + margin-bottom: @halfSpace; + + .button { + float: left; + margin-left: @quarterSpace; + margin-right: @quarterSpace; + padding: @halfSpace; + border-radius: @halfSpace; + background-color: #dddddd; + } + + .buttonRight { + float: right; + } + + .buttonCreate { + background-color: lightgreen; + } + + .buttonCreate:hover { + background-color: limegreen; + } + + .buttonJoin { + background-color: lightskyblue; + } + + .buttonJoin:hover { + background-color: dodgerblue; + } + + .buttonNext { + background-color: lightgreen; + } + + .buttonNext:hover { + background-color: limegreen; + } + + .buttonDelete { + background-color: indianred; + } + + .buttonDelete:hover { + background-color: palevioletred; + } + +} diff --git a/src/main/angular/src/config.less b/src/main/angular/src/config.less index b5b354c..f62f98e 100644 --- a/src/main/angular/src/config.less +++ b/src/main/angular/src/config.less @@ -1,2 +1,3 @@ @space: 0.5em; @halfSpace: calc(@space / 2); +@quarterSpace: calc(@halfSpace / 2); diff --git a/src/main/java/de/ph87/tools/group/AdminRemoveResult.java b/src/main/java/de/ph87/tools/group/AdminRemoveResult.java new file mode 100644 index 0000000..d7055e0 --- /dev/null +++ b/src/main/java/de/ph87/tools/group/AdminRemoveResult.java @@ -0,0 +1,23 @@ +package de.ph87.tools.group; + +import de.ph87.tools.user.User; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +public class AdminRemoveResult { + + @NonNull + public final Group group; + + @NonNull + public final User kicked; + + public AdminRemoveResult(@NonNull final GroupAccess access, @NonNull final User kicked) { + this.group = access.group; + this.kicked = kicked; + } + +} diff --git a/src/main/java/de/ph87/tools/group/Group.java b/src/main/java/de/ph87/tools/group/Group.java index 9ed3fc9..41bfdb8 100644 --- a/src/main/java/de/ph87/tools/group/Group.java +++ b/src/main/java/de/ph87/tools/group/Group.java @@ -55,8 +55,15 @@ public class Group extends GroupAbstract implements IWebSocketMessage { @NonNull @ManyToMany @ToString.Exclude + @JoinTable(name = "`group_user`") private Set users = new HashSet<>(); + @NonNull + @ManyToMany + @ToString.Exclude + @JoinTable(name = "`group_banned`") + private Set banned = new HashSet<>(); + @Setter @NonNull @Column(nullable = false) @@ -89,4 +96,8 @@ public class Group extends GroupAbstract implements IWebSocketMessage { return List.of("Number", uuid); } + public boolean isBanned(@NonNull final User user) { + return banned.stream().anyMatch(u -> u.equals(user)); + } + } diff --git a/src/main/java/de/ph87/tools/group/GroupAccessService.java b/src/main/java/de/ph87/tools/group/GroupAccessService.java index af00e0b..5072d51 100644 --- a/src/main/java/de/ph87/tools/group/GroupAccessService.java +++ b/src/main/java/de/ph87/tools/group/GroupAccessService.java @@ -36,7 +36,7 @@ public class GroupAccessService { } public void delete(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) { - final GroupAccess groupAccess = accessAsOwner(userPrivateUuid, groupUuid); + final GroupAccess groupAccess = adminAccess(userPrivateUuid, groupUuid); numbersRepository.deleteAllByGroup(groupAccess.group); groupRepository.delete(groupAccess.group); log.info("Group deleted: group={}", groupAccess.group); @@ -50,7 +50,7 @@ public class GroupAccessService { } @NonNull - public GroupAccess accessAsOwner(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) { + public GroupAccess adminAccess(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) { final GroupAccess groupAccess = access(userPrivateUuid, groupUuid); if (!groupAccess.group.isOwnedBy(groupAccess.user)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST); diff --git a/src/main/java/de/ph87/tools/group/GroupController.java b/src/main/java/de/ph87/tools/group/GroupController.java index 77ef264..1015cdf 100644 --- a/src/main/java/de/ph87/tools/group/GroupController.java +++ b/src/main/java/de/ph87/tools/group/GroupController.java @@ -3,6 +3,7 @@ package de.ph87.tools.group; import de.ph87.tools.group.requests.GroupChangePasswordRequest; import de.ph87.tools.group.requests.GroupChangeTitleRequest; import de.ph87.tools.group.requests.GroupJoinRequest; +import de.ph87.tools.group.requests.GroupUserRequest; import de.ph87.tools.group.uuid.GroupUuid; import de.ph87.tools.user.uuid.UserPrivateUuid; import de.ph87.tools.user.uuid.UserPublicUuid; @@ -61,6 +62,21 @@ public class GroupController { return groupService.join(privateUuid, request, response); } + @PostMapping("kick") + public void kick(@NonNull final UserPrivateUuid privateUuid, @NonNull @RequestBody final GroupUserRequest request) { + groupService.kick(privateUuid, request); + } + + @PostMapping("ban") + public void ban(@NonNull final UserPrivateUuid privateUuid, @NonNull @RequestBody final GroupUserRequest request) { + groupService.ban(privateUuid, request); + } + + @PostMapping("unban") + public void unban(@NonNull final UserPrivateUuid privateUuid, @NonNull @RequestBody final GroupUserRequest request) { + groupService.unban(privateUuid, request); + } + @PostMapping("changeTitle") public GroupDto changeTitle(@NonNull final UserPrivateUuid privateUuid, @NonNull @RequestBody final GroupChangeTitleRequest request) { return groupService.changeTitle(privateUuid, request); diff --git a/src/main/java/de/ph87/tools/group/GroupDto.java b/src/main/java/de/ph87/tools/group/GroupDto.java index 07e6788..289f605 100644 --- a/src/main/java/de/ph87/tools/group/GroupDto.java +++ b/src/main/java/de/ph87/tools/group/GroupDto.java @@ -34,15 +34,19 @@ public class GroupDto { @NonNull public final Set users; + @NonNull + public final Set banned; + public final boolean initial; - public GroupDto(@NonNull final Group group, @NonNull final UserPublicDto owner, @NonNull final Set users) { + public GroupDto(@NonNull final Group group, @NonNull final UserPublicDto owner, @NonNull final Set users, @NonNull final Set banned) { this.uuid = group.getUuid(); this.title = group.getTitle(); this.created = group.getCreated(); this.password = group.getPassword(); this.owner = owner; this.users = users; + this.banned = banned; this.initial = group.isInitial(); } diff --git a/src/main/java/de/ph87/tools/group/GroupService.java b/src/main/java/de/ph87/tools/group/GroupService.java index cfbf00a..92d8867 100644 --- a/src/main/java/de/ph87/tools/group/GroupService.java +++ b/src/main/java/de/ph87/tools/group/GroupService.java @@ -3,6 +3,7 @@ package de.ph87.tools.group; import de.ph87.tools.group.requests.GroupChangePasswordRequest; import de.ph87.tools.group.requests.GroupChangeTitleRequest; import de.ph87.tools.group.requests.GroupJoinRequest; +import de.ph87.tools.group.requests.GroupUserRequest; import de.ph87.tools.group.uuid.GroupUuid; import de.ph87.tools.user.User; import de.ph87.tools.user.UserPublicDto; @@ -41,7 +42,7 @@ public class GroupService { public GroupDto create(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) { final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response); final Group group = createUnchecked(user); - return doJoinUnchecked(group, user); + return _join_unchecked(group, user); } @NonNull @@ -53,22 +54,69 @@ public class GroupService { @NonNull public GroupDto join(@Nullable final UserPrivateUuid privateUuid, @NonNull final GroupJoinRequest request, @NonNull final HttpServletResponse response) { final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response); - final Group group = getByGroupByGroupUuid(request.uuid); + final Group group = getGroupByGroupUuid(request.groupUuid); + if (group.isBanned(user)) { + log.error("User is banned from Group and cannot join it: user={}, group={}", user, group); + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } if (!group.getPassword().equals(request.password)) { log.error("Wrong password: user={}, group={}", user, group); throw new ResponseStatusException(HttpStatus.FORBIDDEN); } - return doJoinUnchecked(group, user); + return _join_unchecked(group, user); + } + + public void leave(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) { + final User user = userService.access(privateUuid); + final Group group = getGroupByGroupUuid(groupUuid); + if (group.isOwnedBy(user)) { + // owner cannot remove itself from group + throw new ResponseStatusException(HttpStatus.BAD_REQUEST); + } + doLeaveUnchecked(group, user); + } + + public void kick(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) { + final AdminRemoveResult result = adminRemoveUser(privateUuid, request); + log.info("User kicked out of group: user={}, group={}", result.kicked, result.group); + publish(result.group); + } + + public void ban(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) { + final AdminRemoveResult result = adminRemoveUser(privateUuid, request); + result.group.getBanned().add(result.kicked); + log.info("User banned from group: user={}, group={}", result.kicked, result.group); + publish(result.group); + } + + public void unban(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) { + final GroupAccess access = groupAccessService.adminAccess(privateUuid, request.groupUuid); + final User user = access.group.getBanned().stream().filter(u -> u.getPublicUuid().equals(request.userPublicUuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST)); + access.group.getBanned().remove(user); + log.info("User unbanned from group: user={}, group={}", user, access.group); + publish(access.group); } @NonNull - private Group getByGroupByGroupUuid(@NonNull final GroupUuid groupUuid) { + private AdminRemoveResult adminRemoveUser(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request) { + final GroupAccess access = groupAccessService.adminAccess(privateUuid, request.groupUuid); + final User user = access.group.getUsers().stream().filter(u -> u.getPublicUuid().equals(request.userPublicUuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST)); + if (user.equals(access.user)) { + // owner cannot kick itself from group + throw new ResponseStatusException(HttpStatus.BAD_REQUEST); + } + access.group.getUsers().remove(user); + return new AdminRemoveResult(access, user); + } + + @NonNull + private Group getGroupByGroupUuid(@NonNull final GroupUuid groupUuid) { return groupRepository.findByUuid(groupUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST)); } @NonNull public GroupDto changeTitle(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupChangeTitleRequest request) { - final GroupAccess ug = groupAccessService.access(privateUuid, request.uuid); + final GroupAccess ug = groupAccessService.access(privateUuid, request.groupUuid); if (!ug.group.isOwnedBy(ug.user)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN); } @@ -78,17 +126,11 @@ public class GroupService { @NonNull public GroupDto changePassword(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupChangePasswordRequest request) { - final GroupAccess access = groupAccessService.accessAsOwner(privateUuid, request.uuid); + final GroupAccess access = groupAccessService.adminAccess(privateUuid, request.groupUuid); access.group.setPassword(request.password); return publish(access.group); } - public void leave(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) { - final User user = userService.access(privateUuid); - final Group group = getByGroupByGroupUuid(groupUuid); - doLeaveUnchecked(group, user); - } - /* CREATE, JOIN, LEAVE -------------------------------------------------------------------------- */ @NonNull @@ -99,7 +141,7 @@ public class GroupService { } @NonNull - private GroupDto doJoinUnchecked(@NonNull final Group group, @NonNull final User user) { + private GroupDto _join_unchecked(@NonNull final Group group, @NonNull final User user) { group.getUsers().add(user); group.touch(); user.touch(); @@ -121,7 +163,8 @@ public class GroupService { public GroupDto toDto(@NonNull final Group group) { final UserPublicDto owner = new UserPublicDto(group.getOwner()); final Set users = group.getUsers().stream().map(UserPublicDto::new).collect(Collectors.toSet()); - return new GroupDto(group, owner, users); + final Set banned = group.getBanned().stream().map(UserPublicDto::new).collect(Collectors.toSet()); + return new GroupDto(group, owner, users, banned); } /* PUBLISH -------------------------------------------------------------------------------------- */ diff --git a/src/main/java/de/ph87/tools/group/requests/GroupChangePasswordRequest.java b/src/main/java/de/ph87/tools/group/requests/GroupChangePasswordRequest.java index 5cbe557..67277a0 100644 --- a/src/main/java/de/ph87/tools/group/requests/GroupChangePasswordRequest.java +++ b/src/main/java/de/ph87/tools/group/requests/GroupChangePasswordRequest.java @@ -15,7 +15,7 @@ public class GroupChangePasswordRequest { @NonNull @JsonDeserialize(using = GroupUuidDeserializer.class) - public final GroupUuid uuid; + public final GroupUuid groupUuid; @NonNull public final String password; diff --git a/src/main/java/de/ph87/tools/group/requests/GroupChangeTitleRequest.java b/src/main/java/de/ph87/tools/group/requests/GroupChangeTitleRequest.java index e34e074..207e995 100644 --- a/src/main/java/de/ph87/tools/group/requests/GroupChangeTitleRequest.java +++ b/src/main/java/de/ph87/tools/group/requests/GroupChangeTitleRequest.java @@ -15,7 +15,7 @@ public class GroupChangeTitleRequest { @NonNull @JsonDeserialize(using = GroupUuidDeserializer.class) - public final GroupUuid uuid; + public final GroupUuid groupUuid; @NonNull public final String title; diff --git a/src/main/java/de/ph87/tools/group/requests/GroupJoinRequest.java b/src/main/java/de/ph87/tools/group/requests/GroupJoinRequest.java index fac2df4..451a2b1 100644 --- a/src/main/java/de/ph87/tools/group/requests/GroupJoinRequest.java +++ b/src/main/java/de/ph87/tools/group/requests/GroupJoinRequest.java @@ -15,7 +15,7 @@ public class GroupJoinRequest { @NonNull @JsonDeserialize(using = GroupUuidDeserializer.class) - public final GroupUuid uuid; + public final GroupUuid groupUuid; @NonNull public final String password; diff --git a/src/main/java/de/ph87/tools/group/requests/GroupUserRequest.java b/src/main/java/de/ph87/tools/group/requests/GroupUserRequest.java new file mode 100644 index 0000000..e3a73d5 --- /dev/null +++ b/src/main/java/de/ph87/tools/group/requests/GroupUserRequest.java @@ -0,0 +1,26 @@ +package de.ph87.tools.group.requests; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import de.ph87.tools.group.uuid.GroupUuid; +import de.ph87.tools.group.uuid.GroupUuidDeserializer; +import de.ph87.tools.user.uuid.UserPublicUuid; +import de.ph87.tools.user.uuid.UserPublicUuidDeserializer; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class GroupUserRequest { + + @NonNull + @JsonDeserialize(using = GroupUuidDeserializer.class) + public final GroupUuid groupUuid; + + @NonNull + @JsonDeserialize(using = UserPublicUuidDeserializer.class) + public final UserPublicUuid userPublicUuid; + +} diff --git a/src/main/java/de/ph87/tools/tools/numbers/Numbers.java b/src/main/java/de/ph87/tools/tools/numbers/Numbers.java index db57446..7f868ec 100644 --- a/src/main/java/de/ph87/tools/tools/numbers/Numbers.java +++ b/src/main/java/de/ph87/tools/tools/numbers/Numbers.java @@ -44,6 +44,10 @@ public class Numbers extends NumbersAbstract { @Column(nullable = false) private ZonedDateTime date = ZonedDateTime.now(); + @Column + @Nullable + private ZonedDateTime read = null; + @NonNull @OrderColumn @OneToMany(orphanRemoval = true) @@ -54,6 +58,13 @@ public class Numbers extends NumbersAbstract { this.users = users; } + public void setRead(@NonNull final ZonedDateTime date) { + if (this.read != null) { + throw new RuntimeException(); + } + this.read = date; + } + @Nullable public Integer getNumberForUser(@NonNull final User user) { for (int userReferenceIndex = 0; userReferenceIndex < users.size(); userReferenceIndex++) { diff --git a/src/main/java/de/ph87/tools/tools/numbers/NumbersController.java b/src/main/java/de/ph87/tools/tools/numbers/NumbersController.java index b928b1a..748752a 100644 --- a/src/main/java/de/ph87/tools/tools/numbers/NumbersController.java +++ b/src/main/java/de/ph87/tools/tools/numbers/NumbersController.java @@ -41,9 +41,9 @@ public class NumbersController { } @NonNull - @PostMapping("byUuid") - public NumbersDto byUuid(@NonNull final UserPrivateUuid privateUuid, final NumbersUuid numbersUuid) { - return numbersService.dtoByUuid(privateUuid, numbersUuid); + @PostMapping("fetchAndMarkAsRead") + public NumbersDto fetchAndMarkAsRead(@NonNull final UserPrivateUuid privateUuid, final NumbersUuid numbersUuid) { + return numbersService.fetchAndMarkAsRead(privateUuid, numbersUuid); } } diff --git a/src/main/java/de/ph87/tools/tools/numbers/NumbersDto.java b/src/main/java/de/ph87/tools/tools/numbers/NumbersDto.java index 495d762..a3857c8 100644 --- a/src/main/java/de/ph87/tools/tools/numbers/NumbersDto.java +++ b/src/main/java/de/ph87/tools/tools/numbers/NumbersDto.java @@ -26,12 +26,16 @@ public class NumbersDto extends NumbersAbstract { @NonNull private final ZonedDateTime date; + @Nullable + private final ZonedDateTime read; + @Nullable private final Integer number; public NumbersDto(@NonNull final Numbers numbers, @NonNull final GroupDto group, @Nullable final Integer number) { this.uuid = numbers.getUuid(); this.date = numbers.getDate(); + this.read = numbers.getRead(); this.group = group; this.number = number; } diff --git a/src/main/java/de/ph87/tools/tools/numbers/NumbersRepository.java b/src/main/java/de/ph87/tools/tools/numbers/NumbersRepository.java index 06ec9df..fa97e8d 100644 --- a/src/main/java/de/ph87/tools/tools/numbers/NumbersRepository.java +++ b/src/main/java/de/ph87/tools/tools/numbers/NumbersRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.ListCrudRepository; +import java.util.List; + public interface NumbersRepository extends ListCrudRepository { @NonNull @@ -13,4 +15,6 @@ public interface NumbersRepository extends ListCrudRepository { void deleteAllByGroup(@NonNull Group group); + List findAllByGroupAndReadNull(@NonNull Group group); + } diff --git a/src/main/java/de/ph87/tools/tools/numbers/NumbersService.java b/src/main/java/de/ph87/tools/tools/numbers/NumbersService.java index abd5d11..6bb7b44 100644 --- a/src/main/java/de/ph87/tools/tools/numbers/NumbersService.java +++ b/src/main/java/de/ph87/tools/tools/numbers/NumbersService.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -47,7 +48,7 @@ public class NumbersService { private final UserService userService; public void create(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) { - final GroupAccess access = groupAccessService.accessAsOwner(userPrivateUuid, groupUuid); + final GroupAccess access = groupAccessService.adminAccess(userPrivateUuid, groupUuid); final List users = access.getGroup().getUsers().stream().map(userReferenceService::create).collect(Collectors.toList()); Collections.shuffle(users); final Numbers numbers = numbersRepository.save(new Numbers(access.getGroup(), users)); @@ -88,8 +89,10 @@ public class NumbersService { } @NonNull - public NumbersDto dtoByUuid(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) { + public NumbersDto fetchAndMarkAsRead(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) { final NumbersAccess access = access(privateUuid, numbersUuid); + final ZonedDateTime now = ZonedDateTime.now(); + numbersRepository.findAllByGroupAndReadNull(access.numbers.getGroup()).forEach(numbers -> numbers.setRead(now)); return toDto(access.numbers, access.user); } diff --git a/src/main/java/de/ph87/tools/user/UserPrivateDto.java b/src/main/java/de/ph87/tools/user/UserPrivateDto.java index e9e1c11..38ac322 100644 --- a/src/main/java/de/ph87/tools/user/UserPrivateDto.java +++ b/src/main/java/de/ph87/tools/user/UserPrivateDto.java @@ -35,12 +35,15 @@ public class UserPrivateDto extends UserPublicAbstract { private final boolean password; + private final boolean admin; + public UserPrivateDto(@NonNull final User user) { this.publicUuid = user.getPublicUuid(); this.privateUuid = user.getPrivateUuid(); this.name = user.getName(); this.created = user.getCreated(); this.password = !user.getPassword().isEmpty(); + this.admin = user.isAdmin(); } @Nullable diff --git a/src/main/java/de/ph87/tools/user/UserPublicDto.java b/src/main/java/de/ph87/tools/user/UserPublicDto.java index 83d5b58..b9121e2 100644 --- a/src/main/java/de/ph87/tools/user/UserPublicDto.java +++ b/src/main/java/de/ph87/tools/user/UserPublicDto.java @@ -19,9 +19,12 @@ public class UserPublicDto extends UserPublicAbstract { @NonNull public final String name; + public final boolean admin; + public UserPublicDto(@NonNull final User user) { this.publicUuid = user.getPublicUuid(); this.name = user.getName(); + this.admin = user.isAdmin(); } } diff --git a/src/main/java/de/ph87/tools/user/uuid/UserPrivateUuidArgumentResolver.java b/src/main/java/de/ph87/tools/user/uuid/UserPrivateUuidArgumentResolver.java index 0b4f042..c2f35b6 100644 --- a/src/main/java/de/ph87/tools/user/uuid/UserPrivateUuidArgumentResolver.java +++ b/src/main/java/de/ph87/tools/user/uuid/UserPrivateUuidArgumentResolver.java @@ -31,7 +31,14 @@ public class UserPrivateUuidArgumentResolver implements HandlerMethodArgumentRes if (!(webRequest.getNativeRequest() instanceof final HttpServletRequest request)) { throw new RuntimeException(); } - final String uuid = Arrays.stream(request.getCookies()).filter(cookie -> USER_UUID_COOKIE_NAME.equalsIgnoreCase(cookie.getName())).findFirst().map(Cookie::getValue).orElse(null); + final Cookie[] cookies = request.getCookies(); + if (cookies == null) { + if (!nullable) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + return null; + } + final String uuid = Arrays.stream(cookies).filter(cookie -> USER_UUID_COOKIE_NAME.equalsIgnoreCase(cookie.getName())).findFirst().map(Cookie::getValue).orElse(null); if (uuid == null || uuid.length() != 36) { if (!nullable) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); diff --git a/src/main/java/de/ph87/tools/user/uuid/UserPublicUuidDeserializer.java b/src/main/java/de/ph87/tools/user/uuid/UserPublicUuidDeserializer.java new file mode 100644 index 0000000..3b6e1e2 --- /dev/null +++ b/src/main/java/de/ph87/tools/user/uuid/UserPublicUuidDeserializer.java @@ -0,0 +1,18 @@ +package de.ph87.tools.user.uuid; + +import de.ph87.tools.common.uuid.AbstractUuidDeserializer; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Getter +@NoArgsConstructor +public class UserPublicUuidDeserializer extends AbstractUuidDeserializer { + + @NonNull + @Override + protected UserPublicUuid create(@NonNull final String s) { + return new UserPublicUuid(s); + } + +}