Compare commits

..

3 Commits

Author SHA1 Message Date
8b3cf7bbb9 database 2024-10-29 11:14:41 +01:00
76586c720c user table name + spring-boot++ 2024-10-29 08:20:48 +01:00
e45117b57f database WIP 2024-10-24 16:38:36 +02:00
66 changed files with 1163 additions and 894 deletions

View File

@ -5,4 +5,4 @@ spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
#-
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto=create

15
pom.xml
View File

@ -19,7 +19,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<version>3.3.5</version>
</parent>
<dependencies>
@ -32,6 +32,19 @@
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@ -1,20 +0,0 @@
import {UserPublic} from "../User/UserPublic";
export abstract class AbstractSession {
protected constructor(
readonly type: string,
readonly typeDisplayName: string,
readonly uuid: string,
readonly owner: UserPublic,
readonly created: Date,
readonly title: string,
readonly password: string,
readonly users: UserPublic[],
readonly initial: boolean,
) {
// -
}
}

View File

@ -1,70 +0,0 @@
import {ApiService} from "../common/api.service";
import {FromJson, Next} from "../common/types";
import {AbstractSession} from "./AbstractSession";
import {Router} from "@angular/router";
import {UserService} from "../User/user.service";
import {validateBoolean} from "../common/validators";
export abstract class AbstractSessionService<SESSION extends AbstractSession> {
protected constructor(
protected readonly api: ApiService,
protected readonly router: Router,
protected readonly userService: UserService,
readonly apiPath: any[],
readonly routerPath: any[],
readonly fromJson: FromJson<SESSION>,
) {
// -
}
canAccess(uuid: string, next: Next<boolean>): void {
this.api.postSingle([...this.apiPath, 'canAccess'], uuid, validateBoolean, next);
}
get(uuid: string, next: Next<SESSION>): void {
this.api.postSingle([...this.apiPath, 'get'], uuid, this.fromJson, next);
}
create(next: Next<SESSION>): void {
this.api.getSingle([...this.apiPath, 'create'], this.fromJson, session => {
next(session);
this.userService.refresh();
});
}
changeTitle(session: SESSION, title: string, next?: Next<SESSION>) {
const data = {
uuid: session.uuid,
title: title,
};
this.api.postSingle([...this.apiPath, 'changeTitle'], data, this.fromJson, next);
}
changePassword(session: SESSION, password: string, next?: Next<SESSION>) {
const data = {
uuid: session.uuid,
password: password,
};
this.api.postSingle([...this.apiPath, 'changePassword'], data, this.fromJson, next);
}
join(uuid: string, password: string, next: Next<SESSION>): void {
const data = {
uuid: uuid,
password: password,
};
this.api.postSingle([...this.apiPath, 'join'], data, this.fromJson, next);
}
leave(session: SESSION, next: Next<void>): void {
this.api.postNone([...this.apiPath, 'leave'], session, next);
}
goto(session: SESSION) {
const url = '/' + this.routerPath.join('/');
this.router.navigate([url, {uuid: session.uuid}]);
}
}

View File

@ -1,14 +0,0 @@
import {AbstractSession} from "./AbstractSession";
import {validateString} from "../common/validators";
import {NumbersSession} from "../tools/Numbers/Session/NumbersSession";
export function sessionFromJsonOrNull(json: any): AbstractSession | null {
const type = validateString(json['type']);
switch (type) {
case 'Numbers':
return NumbersSession.fromJson(json);
default:
console.error("Not implemented: AbstractSession.type=" + type);
return null;
}
}

View File

@ -1,6 +1,5 @@
import {validateListIgnoreNullItems, validateString} from "../common/validators";
import {AbstractSession} from "../Session/AbstractSession";
import {sessionFromJsonOrNull} from "../Session/sessionFromJsonOrNull";
import {validateList, validateString} from "../common/validators";
import {Group} from "../group/Group";
import {UserPublic} from "./UserPublic";
export class UserCommon extends UserPublic {
@ -8,7 +7,7 @@ export class UserCommon extends UserPublic {
constructor(
publicUuid: string,
name: string,
readonly commonSessions: AbstractSession[],
readonly commonGroups: Group[],
) {
super(publicUuid, name);
}
@ -17,7 +16,7 @@ export class UserCommon extends UserPublic {
return new UserCommon(
validateString(json['publicUuid']),
validateString(json['name']),
validateListIgnoreNullItems(json['commonSessions'], sessionFromJsonOrNull),
validateList(json['commonGroups'], Group.fromJson),
);
}

View File

@ -1,7 +1,5 @@
import {validateDate, validateListIgnoreNullItems, validateString} from "../common/validators";
import {AbstractSession} from "../Session/AbstractSession";
import {sessionFromJsonOrNull} from "../Session/sessionFromJsonOrNull";
import {validateDate, validateList, validateString} from "../common/validators";
import {Group} from "../group/Group";
export class UserPrivate {
@ -9,7 +7,7 @@ export class UserPrivate {
readonly privateUuid: string,
readonly publicUuid: string,
readonly created: Date,
readonly sessions: AbstractSession[],
readonly groups: Group[],
readonly name: string,
) {
// -
@ -20,7 +18,7 @@ export class UserPrivate {
validateString(json['privateUuid']),
validateString(json['publicUuid']),
validateDate(json['created']),
validateListIgnoreNullItems(json['sessions'], sessionFromJsonOrNull),
validateList(json['groups'], Group.fromJson),
validateString(json['name']),
);
}

View File

@ -6,7 +6,7 @@ import {UserCommon} from "./UserCommon";
import {UserPublic} from "./UserPublic";
import {Router} from "@angular/router";
import {BehaviorSubject, Subscription} from "rxjs";
import {AbstractSession} from "../Session/AbstractSession";
import {Group} from "../group/Group";
import {StompService} from "@stomp/ng2-stompjs";
@Injectable({
@ -60,8 +60,7 @@ export class UserService {
return this.subject.subscribe(next);
}
owns(session: AbstractSession): boolean {
return this.user?.publicUuid === session.owner.publicUuid;
owns(group: Group): boolean {
return this.user?.publicUuid === group.owner.publicUuid;
}
}

View File

@ -0,0 +1,35 @@
import {UserPublic} from "../User/UserPublic";
import {validateBoolean, validateDate, validateList, validateString} from "../common/validators";
export class Group {
protected constructor(
readonly uuid: string,
readonly owner: UserPublic,
readonly created: Date,
readonly title: string,
readonly password: string,
readonly users: UserPublic[],
readonly initial: boolean,
) {
// -
}
static fromJson(json: any): Group {
return new Group(
validateString(json['uuid']),
UserPublic.fromJson(json['owner']),
validateDate(json['created']),
validateString(json['title']),
validateString(json['password']),
validateList(json['users'], UserPublic.fromJson),
validateBoolean(json['initial']),
);
}
static compareCreated(a: Group, b: Group): number {
return a.created.getTime() - b.created.getTime();
}
}

View File

@ -0,0 +1,70 @@
import {ApiService} from "../common/api.service";
import {Next} from "../common/types";
import {Group} from "./Group";
import {Router} from "@angular/router";
import {UserService} from "../User/user.service";
import {validateBoolean} from "../common/validators";
import {Injectable} from "@angular/core";
@Injectable({
providedIn: 'root'
})
export class GroupService {
protected constructor(
protected readonly api: ApiService,
protected readonly router: Router,
protected readonly userService: UserService,
) {
// -
}
canAccess(uuid: string, next: Next<boolean>): void {
this.api.postSingle(['Group', 'canAccess'], uuid, validateBoolean, next);
}
get(uuid: string, next: Next<Group>): void {
this.api.postSingle(['Group', 'get'], uuid, Group.fromJson, next);
}
create(next: Next<Group>): void {
this.api.getSingle(['Group', 'create'], Group.fromJson, group => {
next(group);
this.userService.refresh();
});
}
changeTitle(group: Group, title: string, next?: Next<Group>) {
const data = {
uuid: group.uuid,
title: title,
};
this.api.postSingle(['Group', 'changeTitle'], data, Group.fromJson, next);
}
changePassword(group: Group, password: string, next?: Next<Group>) {
const data = {
uuid: group.uuid,
password: password,
};
this.api.postSingle(['Group', 'changePassword'], data, Group.fromJson, next);
}
join(uuid: string, password: string, next: Next<Group>): void {
const data = {
uuid: uuid,
password: password,
};
this.api.postSingle(['Group', 'join'], data, Group.fromJson, next);
}
leave(group: Group, next: Next<void>): void {
this.api.postNone(['Group', 'leave'], group, next);
}
goto(group: Group) {
this.router.navigate(['Group', {uuid: group.uuid}]);
}
}

View File

@ -0,0 +1,30 @@
import {UserPublic} from "../../User/UserPublic";
import {validateBoolean, validateDate, validateList, validateString} from "../../common/validators";
export class Numbers {
constructor(
readonly uuid: string,
readonly title: string,
readonly owner: UserPublic,
readonly created: Date,
readonly password: string,
readonly users: UserPublic[],
readonly initial: boolean,
) {
// -
}
static fromJson(json: any): Numbers {
return new Numbers(
validateString(json['uuid']),
validateString(json['title']),
UserPublic.fromJson(json['owner']),
validateDate(json['created']),
validateString(json['password']),
validateList(json['users'], UserPublic.fromJson),
validateBoolean(json['initial']),
);
}
}

View File

@ -1,40 +0,0 @@
import {UserPublic} from "../../../User/UserPublic";
import {validateBoolean, validateDate, validateList, validateString} from "../../../common/validators";
import {AbstractSession} from "../../../Session/AbstractSession";
export class NumbersSession extends AbstractSession {
static readonly TYPE = 'Numbers';
static readonly TYPE_DISPLAY_NAME = 'Nummern';
constructor(
type: string,
uuid: string,
title: string,
owner: UserPublic,
created: Date,
password: string,
users: UserPublic[],
initial: boolean,
) {
if (type !== NumbersSession.TYPE) {
throw new Error();
}
super(NumbersSession.TYPE, NumbersSession.TYPE_DISPLAY_NAME, uuid, owner, created, title, password, users, initial);
}
static fromJson(json: any): NumbersSession {
return new NumbersSession(
validateString(json['type']),
validateString(json['uuid']),
validateString(json['title']),
UserPublic.fromJson(json['owner']),
validateDate(json['created']),
validateString(json['password']),
validateList(json['users'], UserPublic.fromJson),
validateBoolean(json['initial']),
);
}
}

View File

@ -1,21 +0,0 @@
import {Injectable} from '@angular/core';
import {AbstractSessionService} from "../../../Session/AbstractSessionService";
import {NumbersSession} from "./NumbersSession";
import {ApiService} from "../../../common/api.service";
import {Router} from "@angular/router";
import {UserService} from "../../../User/user.service";
@Injectable({
providedIn: 'root'
})
export class NumbersSessionService extends AbstractSessionService<NumbersSession> {
constructor(
api: ApiService,
router: Router,
userService: UserService
) {
super(api, router, userService, ['Numbers'], ['Numbers', 'Session'], NumbersSession.fromJson);
}
}

View File

@ -0,0 +1,19 @@
import {Injectable} from '@angular/core';
import {ApiService} from "../../common/api.service";
import {Router} from "@angular/router";
import {UserService} from "../../User/user.service";
@Injectable({
providedIn: 'root'
})
export class NumbersService {
constructor(
protected readonly api: ApiService,
protected readonly router: Router,
protected readonly userService: UserService
) {
// -
}
}

View File

@ -1,7 +1,7 @@
<div id="mainMenu" *ngIf="menuVisible">
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/SolarSystem">Planeten</div>
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/VoltageDrop">Leitung</div>
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/Numbers">{{ NumbersSession.TYPE_DISPLAY_NAME }}</div>
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/Groups">Gruppen</div>
<ng-container *ngIf="user !== null">
<div class="mainMenuItem mainMenuItemRight" routerLinkActive="mainMenuItemActive" routerLink="/Profile">{{ user.name }}</div>
</ng-container>

View File

@ -2,7 +2,7 @@ import {Component, OnDestroy, OnInit} from '@angular/core';
import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {NgIf} from "@angular/common";
import {UserService} from "./api/User/user.service";
import {NumbersSession} from "./api/tools/Numbers/Session/NumbersSession";
import {Numbers} from "./api/tools/Numbers/Numbers";
import {UserPrivate} from "./api/User/UserPrivate";
import {Subscription} from "rxjs";
@ -15,7 +15,7 @@ import {Subscription} from "rxjs";
})
export class AppComponent implements OnInit, OnDestroy {
protected readonly NumbersSession = NumbersSession;
protected readonly NumbersGroup = Numbers;
private readonly subscriptions: Subscription[] = [];

View File

@ -2,10 +2,10 @@ import {Routes} from '@angular/router';
import {SolarSystemComponent} from './pages/tools/solar-system/solar-system.component';
import {VoltageDropComponent} from "./pages/tools/voltage-drop/voltage-drop.component";
import {SolarSystemPrintoutComponent} from "./pages/tools/solar-system/printout/solar-system-printout.component";
import {NumbersOverviewComponent} from "./pages/tools/numbers/overview/numbers-overview.component";
import {ProfileComponent} from "./pages/profile/profile.component";
import {NumbersSessionComponent} from "./pages/tools/numbers/session/numbers-session.component";
import {UserComponent} from "./pages/user/user.component";
import {GroupsComponent} from "./pages/group/groups/groups.component";
import {GroupComponent} from "./pages/group/group/group.component";
export const routes: Routes = [
{path: 'SolarSystemPrintout', component: SolarSystemPrintoutComponent},
@ -13,8 +13,8 @@ export const routes: Routes = [
{path: 'VoltageDrop', component: VoltageDropComponent},
{path: 'Numbers', component: NumbersOverviewComponent},
{path: 'Numbers/Session', component: NumbersSessionComponent},
{path: 'Groups', component: GroupsComponent},
{path: 'Group', component: GroupComponent},
{path: 'User', component: UserComponent},

View File

@ -0,0 +1,38 @@
<ng-container *ngIf="group !== null">
<h1>Nummern</h1>
<table>
<tr>
<th>Erstellt</th>
<td>{{ group.created | date:'yyyy-MM-dd HH:mm' }}</td>
</tr>
<tr>
<th>Titel</th>
<td>
<app-text *ngIf="userService.owns(group)" [initial]="group.title" (onChange)="changeTitle(group, $event)"></app-text>
<ng-container *ngIf="!userService.owns(group)"></ng-container>
</td>
</tr>
<tr>
<th>Passwort</th>
<td>
<app-text *ngIf="userService.owns(group)" [initial]="group.password" (onChange)="changePassword(group, $event)"></app-text>
<ng-container *ngIf="!userService.owns(group)"></ng-container>
</td>
</tr>
<tr>
<th>Besitzer</th>
<td class="user" (click)="userService.goto(group.owner)">{{ group.owner.name }}</td>
</tr>
<tr>
<th>Teilnehmer</th>
<td>
<div class="user" *ngFor="let user of group.users" (click)="userService.goto(user)">{{ user.name }}</div>
</td>
</tr>
</table>
</ng-container>
<ng-container *ngIf="uuid && !group && accessDenied">
<h1>Passwort</h1>
<input type="text" [(ngModel)]="password" (keydown.enter)="join()">
</ng-container>

View File

@ -0,0 +1 @@
@import "../../../../common.less";

View File

@ -0,0 +1,77 @@
import {Component, OnInit} from '@angular/core';
import {DatePipe, NgForOf, NgIf} from "@angular/common";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {TextComponent} from "../../../shared/text/text.component";
import {Numbers} from "../../../api/tools/Numbers/Numbers";
import {ActivatedRoute, Router} from "@angular/router";
import {GroupService} from "../../../api/group/GroupService";
import {UserService} from "../../../api/User/user.service";
@Component({
selector: 'app-group',
standalone: true,
imports: [
DatePipe,
NgForOf,
NgIf,
ReactiveFormsModule,
TextComponent,
FormsModule
],
templateUrl: './group.component.html',
styleUrl: './group.component.less'
})
export class GroupComponent implements OnInit {
protected group: Numbers | null = null;
protected uuid: string | null = null;
protected accessDenied: boolean | null = null;
protected password: string = "";
constructor(
protected readonly router: Router,
protected readonly activatedRoute: ActivatedRoute,
protected readonly groupService: GroupService,
protected readonly userService: UserService,
) {
// -
}
ngOnInit(): void {
this.password = "";
this.accessDenied = null;
this.activatedRoute.params.subscribe(params => {
this.uuid = params['uuid'];
if (this.uuid) {
this.groupService.canAccess(this.uuid, canAccess => {
this.accessDenied = !canAccess;
if (canAccess && this.uuid) {
this.groupService.get(this.uuid, group => this.setGroup(group));
}
});
}
});
}
private setGroup(group: Numbers): void {
this.group = group;
}
protected join() {
if (this.uuid) {
this.groupService.join(this.uuid, this.password, group => this.setGroup(group));
}
}
protected changeTitle(group: Numbers, title: string) {
this.groupService.changeTitle(group, title, group => this.setGroup(group));
}
protected changePassword(group: Numbers, password: string) {
this.groupService.changePassword(group, password, group => this.setGroup(group));
}
}

View File

@ -0,0 +1,19 @@
<div class="tileContainer">
<div class="tile">
<div class="tileInner">
<div class="tileTitle">
Gruppen
</div>
<div class="tileContent">
<ng-container *ngIf="userService.user">
<app-group-list [groups]="userService.user.groups"></app-group-list>
</ng-container>
</div>
<div class="tileFooter">
<button (click)="create()">+ Neue Gruppen erstellen</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
@import "../../../../common.less";

View File

@ -0,0 +1,34 @@
import {Component, OnInit} from '@angular/core';
import {GroupListComponent} from "../shared/group-list/group-list.component";
import {UserService} from "../../../api/User/user.service";
import {NgIf} from "@angular/common";
import {GroupService} from "../../../api/group/GroupService";
@Component({
selector: 'app-groups',
standalone: true,
imports: [
GroupListComponent,
NgIf
],
templateUrl: './groups.component.html',
styleUrl: './groups.component.less'
})
export class GroupsComponent implements OnInit {
constructor(
protected readonly userService: UserService,
protected readonly groupService: GroupService,
) {
// -
}
ngOnInit(): void {
this.userService.refresh();
}
create(): void {
this.groupService.create(group => this.groupService.goto(group));
}
}

View File

@ -0,0 +1,10 @@
<table>
<tr>
<th>Titel</th>
<th>Teilnehmer</th>
</tr>
<tr *ngFor="let group of sorted()" (click)="groupService.goto(group)">
<td class="title">{{ group.title }}</td>
<td class="users">{{ group.users.length }}</td>
</tr>
</table>

View File

@ -1,4 +1,4 @@
@import "../../../../../../common.less";
@import "../../../../../common.less";
td {
border: 1px solid black;

View File

@ -0,0 +1,40 @@
import {Component, Input} from '@angular/core';
import {UserService} from "../../../../api/User/user.service";
import {Group} from "../../../../api/group/Group";
import {DatePipe, NgForOf} from "@angular/common";
import {GroupService} from "../../../../api/group/GroupService";
@Component({
selector: 'app-group-list',
standalone: true,
imports: [
DatePipe,
NgForOf
],
templateUrl: './group-list.component.html',
styleUrl: './group-list.component.less'
})
export class GroupListComponent {
@Input()
groups!: Group[];
@Input()
compare: (a: Group, b: Group) => number = Group.compareCreated;
constructor(
protected readonly userService: UserService,
protected readonly groupService: GroupService,
) {
// -
}
protected create() {
this.groupService.create(group => this.groupService.goto(group));
}
protected sorted(): Group[] {
return this.groups.sort(this.compare);
}
}

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="userService.user !== null">
<h1>Profil</h1>
<app-text [initial]="userService.user.name" (onChange)="userService.changeName($event)"></app-text>
<app-session-list [sessions]="userService.user.sessions"></app-session-list>
<app-group-list [groups]="userService.user.groups"></app-group-list>
</ng-container>

View File

@ -3,7 +3,7 @@ import {NgForOf, NgIf} from "@angular/common";
import {UserService} from "../../api/User/user.service";
import {FormsModule} from "@angular/forms";
import {TextComponent} from "../../shared/text/text.component";
import {SessionListComponent} from "../tools/numbers/shared/session-list/session-list.component";
import {GroupListComponent} from "../group/shared/group-list/group-list.component";
@Component({
selector: 'app-profile',
@ -13,7 +13,7 @@ import {SessionListComponent} from "../tools/numbers/shared/session-list/session
NgIf,
FormsModule,
TextComponent,
SessionListComponent
GroupListComponent
],
templateUrl: './profile.component.html',
styleUrl: './profile.component.less'

View File

@ -1,25 +0,0 @@
<div class="tileContainer">
<div class="tile" *ngIf="userService.user">
<div class="tileInner">
<div class="tileTitle">
Teilnahmen
</div>
<div class="tileContent">
<app-session-list [sessions]="userService.user.sessions"></app-session-list>
</div>
</div>
</div>
<div class="tile">
<div class="tileInner">
<div class="tileTitle">
Neues Spiel erstellen
</div>
<div class="tileContent">
<button (click)="create()">+ Neu</button>
</div>
</div>
</div>
</div>

View File

@ -1 +0,0 @@
@import "../../../../../common.less";

View File

@ -1,36 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {UserService} from "../../../../api/User/user.service";
import {DatePipe, NgForOf, NgIf} from "@angular/common";
import {NumbersSessionService} from "../../../../api/tools/Numbers/Session/numbers-session.service";
import {SessionListComponent} from "../shared/session-list/session-list.component";
@Component({
selector: 'app-numbers-overview',
standalone: true,
imports: [
NgForOf,
NgIf,
DatePipe,
SessionListComponent
],
templateUrl: './numbers-overview.component.html',
styleUrl: './numbers-overview.component.less'
})
export class NumbersOverviewComponent implements OnInit {
constructor(
protected readonly userService: UserService,
protected readonly numberSessionService: NumbersSessionService,
) {
// -
}
ngOnInit(): void {
this.userService.refresh();
}
create() {
this.numberSessionService.create(session => this.numberSessionService.goto(session));
}
}

View File

@ -1,38 +0,0 @@
<ng-container *ngIf="session !== null">
<h1>{{ session.typeDisplayName }}</h1>
<table>
<tr>
<th>Erstellt</th>
<td>{{ session.created | date:'yyyy-MM-dd HH:mm' }}</td>
</tr>
<tr>
<th>Titel</th>
<td>
<app-text *ngIf="userService.owns(session)" [initial]="session.title" (onChange)="changeTitle(session, $event)"></app-text>
<ng-container *ngIf="!userService.owns(session)"></ng-container>
</td>
</tr>
<tr>
<th>Passwort</th>
<td>
<app-text *ngIf="userService.owns(session)" [initial]="session.password" (onChange)="changePassword(session, $event)"></app-text>
<ng-container *ngIf="!userService.owns(session)"></ng-container>
</td>
</tr>
<tr>
<th>Besitzer</th>
<td class="user" (click)="userService.goto(session.owner)">{{ session.owner.name }}</td>
</tr>
<tr>
<th>Teilnehmer</th>
<td>
<div class="user" *ngFor="let user of session.users" (click)="userService.goto(user)">{{ user.name }}</div>
</td>
</tr>
</table>
</ng-container>
<ng-container *ngIf="uuid && !session && accessDenied">
<h1>Passwort</h1>
<input type="text" [(ngModel)]="password" (keydown.enter)="join()">
</ng-container>

View File

@ -1 +0,0 @@
@import "../../../../../common.less";

View File

@ -1,76 +0,0 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {NumbersSession} from "../../../../api/tools/Numbers/Session/NumbersSession";
import {DatePipe, NgForOf, NgIf} from "@angular/common";
import {UserService} from "../../../../api/User/user.service";
import {NumbersSessionService} from "../../../../api/tools/Numbers/Session/numbers-session.service";
import {FormsModule} from "@angular/forms";
import {TextComponent} from "../../../../shared/text/text.component";
@Component({
selector: 'app-numbers-session',
standalone: true,
imports: [
NgForOf,
NgIf,
DatePipe,
FormsModule,
TextComponent
],
templateUrl: './numbers-session.component.html',
styleUrl: './numbers-session.component.less'
})
export class NumbersSessionComponent implements OnInit {
protected session: NumbersSession | null = null;
protected uuid: string | null = null;
protected accessDenied: boolean | null = null;
protected password: string = "";
constructor(
protected readonly router: Router,
protected readonly activatedRoute: ActivatedRoute,
protected readonly numbersSessionService: NumbersSessionService,
protected readonly userService: UserService,
) {
// -
}
ngOnInit(): void {
this.password = "";
this.accessDenied = null;
this.activatedRoute.params.subscribe(params => {
this.uuid = params['uuid'];
if (this.uuid) {
this.numbersSessionService.canAccess(this.uuid, canAccess => {
this.accessDenied = !canAccess;
if (canAccess && this.uuid) {
this.numbersSessionService.get(this.uuid, session => this.setSession(session));
}
});
}
});
}
private setSession(session: NumbersSession): void {
this.session = session;
}
protected join() {
if (this.uuid) {
this.numbersSessionService.join(this.uuid, this.password, session => this.setSession(session));
}
}
protected changeTitle(session: NumbersSession, title: string) {
this.numbersSessionService.changeTitle(session, title, session => this.setSession(session));
}
protected changePassword(session: NumbersSession, password: string) {
this.numbersSessionService.changePassword(session, password, session => this.setSession(session));
}
}

View File

@ -1,10 +0,0 @@
<table>
<tr>
<th>Titel</th>
<th>Teilnehmer</th>
</tr>
<tr *ngFor="let session of sessions" (click)="numbersSessionService.goto(session)">
<td class="title">{{ session.title }}</td>
<td class="users">{{ session.users.length }}</td>
</tr>
</table>

View File

@ -1,32 +0,0 @@
import {Component, Input} from '@angular/core';
import {UserService} from "../../../../../api/User/user.service";
import {NumbersSessionService} from "../../../../../api/tools/Numbers/Session/numbers-session.service";
import {AbstractSession} from "../../../../../api/Session/AbstractSession";
import {DatePipe, NgForOf} from "@angular/common";
@Component({
selector: 'app-session-list',
standalone: true,
imports: [
DatePipe,
NgForOf
],
templateUrl: './session-list.component.html',
styleUrl: './session-list.component.less'
})
export class SessionListComponent {
@Input() sessions!: AbstractSession[];
constructor(
protected readonly userService: UserService,
protected readonly numbersSessionService: NumbersSessionService,
) {
// -
}
create() {
this.numbersSessionService.create(session => this.numbersSessionService.goto(session));
}
}

View File

@ -2,5 +2,5 @@
<h2>Benutzer: {{ user.name }}</h2>
<h3>Gemeinsame Sitzungen</h3>
<app-session-list [sessions]="user.commonSessions"></app-session-list>
<app-group-list [groups]="user.commonGroups"></app-group-list>
</ng-container>

View File

@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {NgForOf, NgIf} from "@angular/common";
import {ActivatedRoute} from "@angular/router";
import {UserService} from "../../api/User/user.service";
import {SessionListComponent} from "../tools/numbers/shared/session-list/session-list.component";
import {GroupListComponent} from "../group/shared/group-list/group-list.component";
import {UserCommon} from "../../api/User/UserCommon";
@Component({
@ -11,7 +11,7 @@ import {UserCommon} from "../../api/User/UserCommon";
imports: [
NgForOf,
NgIf,
SessionListComponent
GroupListComponent
],
templateUrl: './user.component.html',
styleUrl: './user.component.less'

View File

@ -31,6 +31,11 @@
}
}
.tileFooter {
padding: @halfSpace @space;
}
}
}

View File

@ -0,0 +1,88 @@
package de.ph87.tools.group;
import de.ph87.tools.user.User;
import de.ph87.tools.web.IWebSocketMessage;
import jakarta.persistence.*;
import lombok.*;
import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Entity
@Getter
@ToString
@NoArgsConstructor
@Table(name = "`group`")
public class Group implements IWebSocketMessage {
@Id
@NonNull
@Column(nullable = false)
private String uuid = UUID.randomUUID().toString();
@NonNull
@ManyToOne(optional = false)
private User owner;
@NonNull
@Column(nullable = false)
private ZonedDateTime created = ZonedDateTime.now();
@NonNull
@ManyToMany
@ToString.Exclude
private Set<User> users = new HashSet<>();
@Setter
@NonNull
@Column(nullable = false)
private String title = "Ohne Namen";
@Setter
@NonNull
@ToString.Exclude
@Column(nullable = false)
private String password = UUID.randomUUID().toString().substring(0, 4);
@Setter
@Column(nullable = false)
private boolean initial = true;
@NonNull
@Column(nullable = false)
private ZonedDateTime lastAccess = created;
protected Group(@NonNull final User owner) {
this.owner = owner;
}
public void touch() {
lastAccess = ZonedDateTime.now();
}
@Override
public List<Object> getWebsocketTopic() {
return List.of("Number", uuid);
}
public boolean isOwnedBy(@NonNull final User user) {
return owner.equals(user);
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof final Group group)) {
return false;
}
return group.uuid.equals(this.uuid);
}
@Override
public int hashCode() {
return uuid.hashCode();
}
}

View File

@ -1,4 +1,4 @@
package de.ph87.tools.session;
package de.ph87.tools.group;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -8,7 +8,7 @@ import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public class SessionChangePasswordInbound {
public class GroupChangePasswordInbound {
@NonNull
public final String uuid;

View File

@ -1,4 +1,4 @@
package de.ph87.tools.session;
package de.ph87.tools.group;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -8,7 +8,7 @@ import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public class SessionChangeTitleInbound {
public class GroupChangeTitleRequest {
@NonNull
public final String uuid;

View File

@ -0,0 +1,54 @@
package de.ph87.tools.group;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import static de.ph87.tools.UserArgumentResolver.USER_UUID_COOKIE_NAME;
@CrossOrigin
@RestController
@RequiredArgsConstructor
@RequestMapping("Group")
public class GroupController {
private final GroupService groupService;
@GetMapping("create")
public GroupDto create(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String privateUuid, @NonNull final HttpServletResponse response) {
return groupService.create(privateUuid, response);
}
@PostMapping("canAccess")
public boolean canAccess(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String privateUuid, @NonNull @RequestBody final String groupUuid) {
return groupService.canAccess(privateUuid, groupUuid);
}
@PostMapping("get")
public GroupDto get(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String privateUuid, @NonNull @RequestBody final String groupUuid) {
return groupService.get(privateUuid, groupUuid);
}
@PostMapping("join")
public GroupDto join(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String privateUuid, @NonNull @RequestBody final GroupJoinRequest request) {
return groupService.join(privateUuid, request);
}
@PostMapping("changeTitle")
public GroupDto changeTitle(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String privateUuid, @NonNull @RequestBody final GroupChangeTitleRequest request) {
return groupService.changeTitle(privateUuid, request);
}
@PostMapping("changePassword")
public GroupDto changePassword(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String privateUuid, @NonNull @RequestBody final GroupChangePasswordInbound request) {
return groupService.changePassword(privateUuid, request);
}
@PostMapping("leave")
public void leave(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String privateUuid, @NonNull @RequestBody final String groupUuid) {
groupService.leave(privateUuid, groupUuid);
}
}

View File

@ -0,0 +1,46 @@
package de.ph87.tools.group;
import de.ph87.tools.user.UserPublicDto;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
@ToString
public class GroupDto {
@NonNull
public final String uuid;
@NonNull
public final String title;
@NonNull
public final ZonedDateTime created;
@NonNull
public final String password;
@NonNull
public final UserPublicDto owner;
@NonNull
public final Set<UserPublicDto> users;
public final boolean initial;
public GroupDto(@NonNull final Group group, @NonNull final UserPublicDto owner) {
this.uuid = group.getUuid();
this.title = group.getTitle();
this.created = group.getCreated();
this.password = group.getPassword();
this.owner = owner;
this.users = group.getUsers().stream().map(UserPublicDto::new).collect(Collectors.toSet());
this.initial = group.isInitial();
}
}

View File

@ -1,4 +1,4 @@
package de.ph87.tools.session;
package de.ph87.tools.group;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -8,7 +8,7 @@ import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public class SessionJoinInbound {
public class GroupJoinRequest {
@NonNull
public final String uuid;

View File

@ -0,0 +1,57 @@
package de.ph87.tools.group;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserPublicDto;
import de.ph87.tools.user.UserPublicMapper;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class GroupMapper {
private final UserPublicMapper userPublicMapper;
private final GroupRepository groupRepository;
private final ApplicationEventPublisher applicationEventPublisher;
/* DTO ------------------------------------------------------------------------------------------ */
@NonNull
public GroupDto toDto(@NonNull final Group group) {
final UserPublicDto owner = userPublicMapper.toPublicDto(group.getOwner());
return new GroupDto(group, owner);
}
/* PUBLISH -------------------------------------------------------------------------------------- */
@NonNull
public GroupDto publish(@NonNull final Group group) {
final GroupDto dto = toDto(group);
applicationEventPublisher.publishEvent(dto);
return dto;
}
/* FIND & GET ----------------------------------------------------------------------------------- */
@NonNull
public Set<GroupDto> findAllByUser(@NonNull final User user) {
return groupRepository.findAllByUsersContains(user).stream().map(this::toDto).collect(Collectors.toSet());
}
@NonNull
public Set<GroupDto> findAllCommonGroups(@NonNull final User a, @NonNull final User b) {
return groupRepository.findAllByUsersContainsAndUsersContains(a, b).stream().map(this::toDto).collect(Collectors.toSet());
}
}

View File

@ -0,0 +1,52 @@
package de.ph87.tools.group;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserService;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class GroupOfUserService {
private final GroupRepository groupRepository;
private final UserService userService;
@NonNull
public Group getGroupOfUser(@NonNull final String groupUuid, @NonNull final User user) {
return findGroupOfUser(groupUuid, user).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@NonNull
public Optional<Group> findGroupOfUser(@NonNull final String groupUuid, @NonNull final User user) {
return groupRepository.findByUuidAndUsersContains(groupUuid, user);
}
@NonNull
public GroupOfUser accessGroupOfUser(@NonNull final String groupUuid, @NonNull final String privateUserUuid) {
final User user = userService.getByPrivateUuidOrThrow(privateUserUuid);
final Group group = getGroupOfUser(groupUuid, user);
return new GroupOfUser(user, group);
}
@Data
public static class GroupOfUser {
public final User user;
public final Group group;
}
}

View File

@ -0,0 +1,24 @@
package de.ph87.tools.group;
import de.ph87.tools.user.User;
import lombok.NonNull;
import org.springframework.data.repository.ListCrudRepository;
import java.util.Optional;
import java.util.Set;
public interface GroupRepository extends ListCrudRepository<Group, String> {
@NonNull
Optional<Group> findByUuid(@NonNull String uuid);
@NonNull
Optional<Group> findByUuidAndUsersContains(final @NonNull String groupUuid, @NonNull User user);
@NonNull
Set<Group> findAllByUsersContains(@NonNull User user);
@NonNull
Set<Group> findAllByUsersContainsAndUsersContains(@NonNull User a, @NonNull User b);
}

View File

@ -0,0 +1,124 @@
package de.ph87.tools.group;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserPublicMapper;
import de.ph87.tools.user.UserService;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class GroupService {
private final GroupRepository groupRepository;
private final UserService userService;
private final GroupMapper groupMapper;
private final UserPublicMapper userPublicMapper;
private final GroupOfUserService groupOfUserService;
@NonNull
public GroupDto create(@Nullable final String privateUuid, @NonNull final HttpServletResponse response) {
final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response);
final Group group = createUnchecked(user);
return doJoinUnchecked(group, user);
}
public boolean canAccess(@Nullable final String privateUuid, @NonNull final String groupUuid) {
if (privateUuid == null) {
return false;
}
return userService.findByPrivateUuid(privateUuid).flatMap(user -> groupOfUserService.findGroupOfUser(groupUuid, user)).isPresent();
}
@NonNull
public GroupDto get(@NonNull final String privateUuid, @NonNull final String groupUuid) {
final GroupOfUserService.GroupOfUser ug = groupOfUserService.accessGroupOfUser(groupUuid, privateUuid);
return groupMapper.toDto(ug.group);
}
@NonNull
public GroupDto join(@NonNull final String privateUuid, @NonNull final GroupJoinRequest request) {
final User user = userService.getByPrivateUuidOrThrow(privateUuid);
final Group group = getByGroupByGroupUuid(request.uuid);
if (!group.getPassword().equals(request.password)) {
log.error("Wrong password: user={}, group={}", user, group);
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return doJoinUnchecked(group, user);
}
@NonNull
private Group getByGroupByGroupUuid(@NonNull final String groupUuid) {
return groupRepository.findByUuid(groupUuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@NonNull
public GroupDto changeTitle(@NonNull final String privateUuid, @NonNull final GroupChangeTitleRequest request) {
final GroupOfUserService.GroupOfUser ug = groupOfUserService.accessGroupOfUser(request.uuid, privateUuid);
if (!ug.group.isOwnedBy(ug.user)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
ug.group.setTitle(request.title);
return groupMapper.publish(ug.group);
}
@NonNull
public GroupDto changePassword(@NonNull final String privateUuid, @NonNull final GroupChangePasswordInbound request) {
final User user = userService.getByPrivateUuidOrThrow(privateUuid);
final Group group = groupOfUserService.getGroupOfUser(request.uuid, user);
if (group.isOwnedBy(user)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
group.setPassword(request.password);
return groupMapper.publish(group);
}
public void leave(@NonNull final String privateUuid, @NonNull final String groupUuid) {
final User user = userService.getByPrivateUuidOrThrow(privateUuid);
final Group group = getByGroupByGroupUuid(groupUuid);
doLeaveUnchecked(group, user);
}
/* CREATE, JOIN, LEAVE -------------------------------------------------------------------------- */
@NonNull
private Group createUnchecked(@NonNull final User user) {
final Group group = groupRepository.save(new Group(user));
log.info("Group CREATED: {}", group);
return group;
}
@NonNull
private GroupDto doJoinUnchecked(@NonNull final Group group, @NonNull final User user) {
group.getUsers().add(user);
group.touch();
user.touch();
log.info("User joined Group: user={}, group={}", user, group);
final GroupDto groupDto = groupMapper.publish(group);
userPublicMapper.publish(user);
return groupDto;
}
private void doLeaveUnchecked(final Group group, final User user) {
group.getUsers().remove(user);
group.touch();
user.touch();
log.info("User left Group: user={}, group={}", user, group);
groupMapper.publish(group);
userPublicMapper.publish(user);
}
}

View File

@ -1,74 +0,0 @@
package de.ph87.tools.session;
import de.ph87.tools.user.User;
import de.ph87.tools.web.IWebSocketMessage;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Getter
@ToString
public abstract class AbstractSession implements IWebSocketMessage {
@NonNull
public final String uuid = UUID.randomUUID().toString();
@NonNull
public final User owner;
@NonNull
public final ZonedDateTime created = ZonedDateTime.now();
@NonNull
@ToString.Exclude
private final Set<User> users = new HashSet<>();
@Setter
@NonNull
public String title = "Spiel ohne Namen";
@Setter
@NonNull
@ToString.Exclude
private String password = UUID.randomUUID().toString().substring(0, 4);
@Setter
private boolean initial = true;
@NonNull
private ZonedDateTime lastAccess = created;
protected AbstractSession(@NonNull final User user) {
this.owner = user;
}
public void join(@NonNull final User user) {
synchronized (uuid) {
users.add(user);
touch();
}
}
public void leave(@NonNull final User user) {
synchronized (uuid) {
users.remove(user);
}
}
private void touch() {
lastAccess = ZonedDateTime.now();
}
@Override
public List<Object> getWebsocketTopic() {
return List.of("Number", uuid);
}
}

View File

@ -1,166 +0,0 @@
package de.ph87.tools.session;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserService;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static de.ph87.tools.UserArgumentResolver.USER_UUID_COOKIE_NAME;
@Slf4j
@CrossOrigin
@EnableScheduling
@RequiredArgsConstructor
public abstract class AbstractSessionController<SESSION extends AbstractSession> {
private final Set<SESSION> sessions = new HashSet<>();
private final UserService userService;
private final ApplicationEventPublisher applicationEventPublisher;
private final Class<SESSION> sessionClazz;
@Scheduled(timeUnit = TimeUnit.MINUTES, initialDelay = 5, fixedRate = 5)
public void cleanUp() {
final ZonedDateTime deadline = ZonedDateTime.now().minusDays(30);
synchronized (sessions) {
sessions.stream().filter(session -> session.getLastAccess().isBefore(deadline)).forEach(this::delete);
}
}
private void delete(@NonNull final SESSION session) {
for (final User user : session.getUsers()) {
user.leave(session);
}
}
@GetMapping("create")
public AbstractSessionDto create(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String userUuid, @NonNull final HttpServletResponse response) {
final User user = userService.getUserByUuidOrElseCreate(userUuid, response);
final Optional<SESSION> existing = user.getSessions().stream().filter(sessionClazz::isInstance).filter(AbstractSession::isInitial).map(sessionClazz::cast).min(Comparator.comparing(AbstractSession::getCreated));
if (existing.isPresent()) {
log.info("User wanted to create new Session but found existing: {}", existing);
return toDto(existing.get(), user);
}
final SESSION session = create(user);
log.info("Session CREATED: {}", session);
synchronized (sessions) {
sessions.add(session);
}
return join(session, user);
}
@PostMapping("canAccess")
public boolean canAccess(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String userUuid, @RequestBody final String sessionUuid) {
if (userUuid == null) {
return false;
}
return userService.findByPrivateUuid(userUuid).flatMap(user -> findUserSession(user, sessionUuid)).isPresent();
}
@PostMapping("get")
public AbstractSessionDto get(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @RequestBody final String sessionUuid) {
final User user = userService.getByPrivateUuidOrThrow(userUuid);
final SESSION session = getUserSession(user, sessionUuid);
return toDto(session, user);
}
@PostMapping("join")
public AbstractSessionDto join(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @RequestBody final SessionJoinInbound inbound) {
final User user = userService.getByPrivateUuidOrThrow(userUuid);
final SESSION session = getSessionByUuid(inbound.uuid);
if (!session.getPassword().equals(inbound.password)) {
log.error("Wrong password: user={}, session={}", user, session);
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return join(session, user);
}
@NonNull
private AbstractSessionDto join(@NonNull final SESSION session, @NonNull final User user) {
session.join(user);
user.join(session);
log.info("User joined Session: user={}, session={}", user, session);
applicationEventPublisher.publishEvent(session);
applicationEventPublisher.publishEvent(user);
return toDto(session, user);
}
@PostMapping("changeTitle")
public AbstractSessionDto changeTitle(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @RequestBody final SessionChangeTitleInbound inbound) {
final User user = userService.getByPrivateUuidOrThrow(userUuid);
final SESSION session = getUserSession(user, inbound.uuid);
if (session.owner != user) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
session.setTitle(inbound.title);
applicationEventPublisher.publishEvent(session);
return toDto(session, user);
}
@PostMapping("changePassword")
public AbstractSessionDto changePassword(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @RequestBody final SessionChangePasswordInbound inbound) {
final User user = userService.getByPrivateUuidOrThrow(userUuid);
final SESSION session = getUserSession(user, inbound.uuid);
if (session.owner != user) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
session.setPassword(inbound.password);
applicationEventPublisher.publishEvent(session);
return toDto(session, user);
}
@PostMapping("leave")
public void leave(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @RequestBody final String sessionUuid) {
final User user = userService.getByPrivateUuidOrThrow(userUuid);
final SESSION session = getSessionByUuid(sessionUuid);
session.leave(user);
user.leave(session);
applicationEventPublisher.publishEvent(session);
applicationEventPublisher.publishEvent(user);
}
@NonNull
private SESSION getUserSession(@NonNull final User user, @NonNull final String sessionUuid) {
return findUserSession(user, sessionUuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@NonNull
private Optional<SESSION> findUserSession(@NonNull final User user, @NonNull final String sessionUuid) {
return user.getSessions().stream().filter(s -> s.uuid.equals(sessionUuid)).filter(sessionClazz::isInstance).map(sessionClazz::cast).findFirst();
}
@NonNull
private SESSION getSessionByUuid(@NonNull final String uuid) {
synchronized (sessions) {
return sessions.stream().filter(u -> u.getUuid().equals(uuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
}
@NonNull
protected abstract SESSION create(@NonNull final User user);
@NonNull
protected abstract AbstractSessionDto toDto(@NonNull final SESSION session, @NonNull final User forUser);
}

View File

@ -1,60 +0,0 @@
package de.ph87.tools.session;
import de.ph87.tools.tools.numbers.NumbersSession;
import de.ph87.tools.tools.numbers.NumbersSessionDto;
import de.ph87.tools.user.UserPublicDto;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
@ToString
public abstract class AbstractSessionDto {
@NonNull
private final String type;
@NonNull
public final String uuid;
@NonNull
public final String title;
@NonNull
public final ZonedDateTime created;
@NonNull
public final String password;
@NonNull
public final UserPublicDto owner;
@NonNull
public final Set<UserPublicDto> users;
public final boolean initial;
protected AbstractSessionDto(@NonNull final AbstractSession session, @NonNull final String type) {
this.type = type;
this.uuid = session.uuid;
this.title = session.title;
this.created = session.created;
this.password = session.getPassword();
this.owner = new UserPublicDto(session.owner);
this.users = session.getUsers().stream().map(UserPublicDto::new).collect(Collectors.toSet());
this.initial = session.isInitial();
}
@NonNull
public static AbstractSessionDto toDto(@NonNull final AbstractSession session) {
return switch (session) {
case NumbersSession numbersSession -> new NumbersSessionDto(numbersSession);
default -> throw new IllegalStateException("No DTO mapping for: " + session);
};
}
}

View File

@ -0,0 +1,49 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.group.Group;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
import java.util.UUID;
@Entity
@Getter
@ToString
@NoArgsConstructor
@Table(name = "`numbers`")
public class Numbers {
@Id
@NonNull
@Column(nullable = false)
private String uuid = UUID.randomUUID().toString();
@NonNull
@Column(nullable = false)
private String name = "Ohne Namen";
@NonNull
@OneToOne
private Group group;
public Numbers(@NonNull final Group group) {
this.group = group;
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof final Numbers numbers)) {
return false;
}
return numbers.uuid.equals(this.uuid);
}
@Override
public int hashCode() {
return uuid.hashCode();
}
}

View File

@ -1,32 +1,14 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.session.AbstractSessionController;
import de.ph87.tools.session.AbstractSessionDto;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserService;
import lombok.NonNull;
import org.springframework.context.ApplicationEventPublisher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@CrossOrigin
@RequiredArgsConstructor
@RequestMapping("Numbers")
public class NumbersController extends AbstractSessionController<NumbersSession> {
public NumbersController(final UserService userService, final ApplicationEventPublisher applicationEventPublisher) {
super(userService, applicationEventPublisher, NumbersSession.class);
}
@NonNull
@Override
protected NumbersSession create(@NonNull final User user) {
return new NumbersSession(user);
}
@NonNull
@Override
protected AbstractSessionDto toDto(@NonNull final NumbersSession session, @NonNull final User forUser) {
return new NumbersSessionDto(session);
}
public class NumbersController {
}

View File

@ -0,0 +1,27 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.group.GroupDto;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString(callSuper = true)
public class NumbersDto {
@NonNull
private final String uuid;
@NonNull
private final String name;
@NonNull
private final GroupDto group;
public NumbersDto(@NonNull final Numbers numbers, @NonNull final GroupDto group) {
this.uuid = numbers.getUuid();
this.name = numbers.getName();
this.group = group;
}
}

View File

@ -1,13 +0,0 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.session.AbstractSession;
import de.ph87.tools.user.User;
import lombok.NonNull;
public class NumbersSession extends AbstractSession {
protected NumbersSession(@NonNull final User user) {
super(user);
}
}

View File

@ -1,16 +0,0 @@
package de.ph87.tools.tools.numbers;
import de.ph87.tools.session.AbstractSessionDto;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString(callSuper = true)
public class NumbersSessionDto extends AbstractSessionDto {
public NumbersSessionDto(@NonNull final NumbersSession session) {
super(session, "Numbers");
}
}

View File

@ -1,61 +1,65 @@
package de.ph87.tools.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import de.ph87.tools.session.AbstractSession;
import de.ph87.tools.web.IWebSocketMessage;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;
import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Entity
@Getter
@ToString
@NoArgsConstructor
@Table(name = "`user`")
public class User implements IWebSocketMessage {
@Id
@NonNull
public final String privateUuid = UUID.randomUUID().toString();
@Column(nullable = false)
private String privateUuid = UUID.randomUUID().toString();
@NonNull
public final String publicUuid = UUID.randomUUID().toString();
@Column(nullable = false)
private String publicUuid = UUID.randomUUID().toString();
public final ZonedDateTime created = ZonedDateTime.now();
@JsonIgnore
@ToString.Exclude
private final Set<AbstractSession> sessions = new HashSet<>();
@NonNull
@Column(nullable = false)
private ZonedDateTime created = ZonedDateTime.now();
@NonNull
@Column(nullable = false)
private ZonedDateTime lastAccess = created;
@Setter
@NonNull
public String name = "unnamed";
@Column(nullable = false)
private String name = "Neuer Benutzer";
private void touch() {
public void touch() {
lastAccess = ZonedDateTime.now();
}
public void join(@NonNull final AbstractSession session) {
synchronized (privateUuid) {
sessions.add(session);
touch();
}
}
public void leave(@NonNull final AbstractSession session) {
synchronized (sessions) {
sessions.remove(session);
}
}
@Override
public List<Object> getWebsocketTopic() {
return List.of("User", privateUuid);
}
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof final User user)) {
return false;
}
return user.privateUuid.equals(this.privateUuid);
}
@Override
public int hashCode() {
return privateUuid.hashCode();
}
}

View File

@ -1,13 +1,11 @@
package de.ph87.tools.user;
import de.ph87.tools.session.AbstractSessionDto;
import jakarta.annotation.Nullable;
import de.ph87.tools.group.GroupDto;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
@ToString
@ -20,15 +18,12 @@ public class UserCommonDto {
public final String name;
@NonNull
public final Set<AbstractSessionDto> commonSessions;
public final Set<GroupDto> commonGroups;
public UserCommonDto(@NonNull final User target, @Nullable final User principal) {
public UserCommonDto(@NonNull final User target, @NonNull final Set<GroupDto> commonGroups) {
this.publicUuid = target.getPublicUuid();
this.name = target.getName();
this.commonSessions = target.getSessions().stream()
.filter(session -> session.getUsers().contains(principal))
.map(AbstractSessionDto::toDto)
.collect(Collectors.toSet());
this.commonGroups = commonGroups;
}
}

View File

@ -21,12 +21,7 @@ public class UserController {
@Nullable
@GetMapping("whoAmI")
public UserPrivateDto whoAmI(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String userUuid, @NonNull final HttpServletResponse response) {
return userService.getUserByUuidOrNull(userUuid, response);
}
@GetMapping("delete")
public void delete(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @NonNull final HttpServletResponse response) {
userService.delete(userUuid, response);
return userService.getUserByPrivateUuidOrNull(userUuid, response);
}
@NonNull
@ -37,8 +32,13 @@ public class UserController {
@NonNull
@PostMapping("getCommonByUuid")
public UserCommonDto getCommonByUuid(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String userUuid, @NonNull @RequestBody final String targetUuid) {
public UserCommonDto getCommonByUuid(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @NonNull @RequestBody final String targetUuid) {
return userService.getCommonByUuid(userUuid, targetUuid);
}
@GetMapping("delete")
public void delete(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @NonNull final HttpServletResponse response) {
userService.delete(userUuid, response);
}
}

View File

@ -1,14 +1,12 @@
package de.ph87.tools.user;
import de.ph87.tools.session.AbstractSessionDto;
import jakarta.annotation.Nullable;
import de.ph87.tools.group.GroupDto;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
@ToString
@ -27,22 +25,14 @@ public class UserPrivateDto {
public final ZonedDateTime created;
@NonNull
private final Set<AbstractSessionDto> sessions;
private final Set<GroupDto> groups;
public UserPrivateDto(@NonNull final User user) {
public UserPrivateDto(@NonNull final User user, final @NonNull Set<GroupDto> groups) {
this.publicUuid = user.getPublicUuid();
this.name = user.getName();
this.privateUuid = user.privateUuid;
this.created = user.created;
this.sessions = user.getSessions().stream().map(AbstractSessionDto::toDto).collect(Collectors.toSet());
}
@Nullable
public static UserPrivateDto orNull(@Nullable final User user) {
if (user == null) {
return null;
}
return new UserPrivateDto(user);
this.privateUuid = user.getPrivateUuid();
this.created = user.getCreated();
this.groups = groups;
}
}

View File

@ -0,0 +1,38 @@
package de.ph87.tools.user;
import de.ph87.tools.group.GroupDto;
import de.ph87.tools.group.GroupMapper;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Set;
@Slf4j
@Service
@Transactional
@EnableScheduling
@RequiredArgsConstructor
public class UserPrivateMapper {
private final GroupMapper groupMapper;
@NonNull
public UserPrivateDto toPrivateDto(@NonNull final User user) {
final Set<GroupDto> groups = groupMapper.findAllByUser(user);
return new UserPrivateDto(user, groups);
}
@Nullable
public UserPrivateDto toPrivateDtoOrNull(@Nullable final User user) {
if (user == null) {
return null;
}
return toPrivateDto(user);
}
}

View File

@ -0,0 +1,30 @@
package de.ph87.tools.user;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@Transactional
@EnableScheduling
@RequiredArgsConstructor
public class UserPublicMapper {
private final ApplicationEventPublisher applicationEventPublisher;
@NonNull
public UserPublicDto toPublicDto(@NonNull final User user) {
return new UserPublicDto(user);
}
public void publish(@NonNull final User user) {
final UserPublicDto dto = toPublicDto(user);
applicationEventPublisher.publishEvent(dto);
}
}

View File

@ -0,0 +1,16 @@
package de.ph87.tools.user;
import lombok.NonNull;
import org.springframework.data.repository.ListCrudRepository;
import java.util.Optional;
public interface UserRepository extends ListCrudRepository<User, String> {
@NonNull
Optional<User> findByPublicUuid(@NonNull String publicUuid);
@NonNull
Optional<User> findByPrivateUuid(@NonNull String privateUuid);
}

View File

@ -1,116 +1,130 @@
package de.ph87.tools.user;
import de.ph87.tools.group.GroupDto;
import de.ph87.tools.group.GroupMapper;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import static de.ph87.tools.UserArgumentResolver.USER_UUID_COOKIE_NAME;
@Slf4j
@Service
@Transactional
@EnableScheduling
@RequiredArgsConstructor
public class UserService {
private final ApplicationEventPublisher applicationEventPublisher;
private final UserRepository userRepository;
private final Set<User> users = new HashSet<>();
private final UserPrivateMapper userPrivateMapper;
private final UserPublicMapper userPublicMapper;
private final GroupMapper groupMapper;
@NonNull
public User getUserByUuidOrElseCreate(@Nullable final String uuid, @NonNull final HttpServletResponse response) {
synchronized (users) {
final User user = Optional.ofNullable(uuid).map(this::findByPrivateUuid).filter(Optional::isPresent).map(Optional::get).orElseGet(this::create);
public User getUserByPrivateUuidOrElseCreate(@Nullable final String privateUuid, @NonNull final HttpServletResponse response) {
final User user = Optional.ofNullable(privateUuid).map(userRepository::findByPrivateUuid).filter(Optional::isPresent).map(Optional::get).orElseGet(this::createUnchecked);
writeUserUuidCookie(response, user);
return user;
}
}
@Nullable
public UserPrivateDto getUserByUuidOrNull(@Nullable final String userUuid, final @NonNull HttpServletResponse response) {
if (userUuid == null || userUuid.isEmpty()) {
public UserPrivateDto getUserByPrivateUuidOrNull(@Nullable final String privateUuid, final @NonNull HttpServletResponse response) {
if (privateUuid == null || privateUuid.isEmpty()) {
return null;
}
final User user = findByPrivateUuid(userUuid).orElse(null);
final User user = userRepository.findByPrivateUuid(privateUuid).orElse(null);
writeUserUuidCookie(response, user);
return UserPrivateDto.orNull(user);
}
public void delete(@NonNull final String userUuid, final @NonNull HttpServletResponse response) {
synchronized (users) {
final User user = findByPrivateUuid(userUuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
users.remove(user);
log.info("User DELETED: {}", user);
}
writeUserUuidCookie(response, null);
return userPrivateMapper.toPrivateDtoOrNull(user);
}
@NonNull
public User getByPrivateUuidOrThrow(@NonNull final String uuid) {
synchronized (users) {
return findByPrivateUuid(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
public UserPrivateDto changeName(@NonNull final String privateUuid, @NonNull final String name) {
return modify(privateUuid, user -> {
user.setName(name);
log.info("User name changed: user={}", user);
}, true);
}
@NonNull
public User getByPublicUuid(@NonNull final String uuid) {
synchronized (users) {
return findByPublicUuid(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
public UserCommonDto getCommonByUuid(@NonNull final String privateUuid, @NonNull final String targetUuid) {
final User principal = getByPrivateUuidOrThrow(privateUuid);
final User target = getByPublicUuid(targetUuid);
final Set<GroupDto> commonGroups = groupMapper.findAllCommonGroups(principal, target);
return new UserCommonDto(target, commonGroups);
}
@NonNull
public Optional<User> findByPrivateUuid(@NonNull final String uuid) {
return users.stream().filter(u -> u.getPrivateUuid().equals(uuid)).findFirst();
public void delete(@NonNull final String privateUuid, final @NonNull HttpServletResponse response) {
final User user = userRepository.findByPrivateUuid(privateUuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
deleteUnchecked(response, user);
}
@NonNull
public Optional<User> findByPublicUuid(@NonNull final String uuid) {
return users.stream().filter(u -> u.getPublicUuid().equals(uuid)).findFirst();
}
/* CREATE, MODIFY, DELETE ----------------------------------------------------------------------- */
@NonNull
private User create() {
final User user = new User();
users.add(user);
private User createUnchecked() {
final User user = userRepository.save(new User());
log.info("User CREATED: {}", user);
return user;
}
@NonNull
private UserPrivateDto modify(@NonNull final String privateUuid, @NonNull Consumer<User> modifier, final boolean doPublish) {
final User user = getByPrivateUuidOrThrow(privateUuid);
modifier.accept(user);
if (doPublish) {
userPublicMapper.publish(user);
}
return userPrivateMapper.toPrivateDto(user);
}
private void deleteUnchecked(final HttpServletResponse response, final User user) {
userRepository.delete(user);
log.info("User DELETED: {}", user);
writeUserUuidCookie(response, null);
}
/* GETTERS & FINDERS ---------------------------------------------------------------------------- */
@NonNull
public User getByPrivateUuidOrThrow(@NonNull final String privateUuid) {
return userRepository.findByPrivateUuid(privateUuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@NonNull
public User getByPublicUuid(@NonNull final String publicUuid) {
return userRepository.findByPublicUuid(publicUuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@NonNull
public Optional<User> findByPrivateUuid(@NonNull final String privateUuid) {
return userRepository.findByPrivateUuid(privateUuid);
}
/* COOKIES -------------------------------------------------------------------------------------- */
private static void writeUserUuidCookie(@NonNull final HttpServletResponse response, @Nullable final User user) {
final Cookie cookie = new Cookie(USER_UUID_COOKIE_NAME, "");
if (user != null) {
cookie.setValue(user.privateUuid);
cookie.setValue(user.getPrivateUuid());
}
cookie.setMaxAge(10 * 365 * 24 * 60 * 60);
cookie.setPath("/");
response.addCookie(cookie);
}
@NonNull
public UserPrivateDto changeName(@NonNull final String uuid, @NonNull final String name) {
final User user = getByPrivateUuidOrThrow(uuid);
user.setName(name);
applicationEventPublisher.publishEvent(new UserPublicDto(user));
return new UserPrivateDto(user);
}
@NonNull
public UserCommonDto getCommonByUuid(@Nullable final String principalUuid, @NonNull final String targetUuid) {
final User principal = principalUuid == null ? null : findByPrivateUuid(principalUuid).orElse(null);
final User target = getByPublicUuid(targetUuid);
return new UserCommonDto(target, principal);
}
}

View File

@ -1,6 +1,10 @@
logging.level.root=WARN
logging.level.de.ph87=INFO
#-
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
#-
spring.jackson.serialization.indent_output=true
#-
spring.main.banner-mode=off