AbstractSession basics + Numbers WIP
This commit is contained in:
parent
43bb77e856
commit
4b2a148ddc
6
pom.xml
6
pom.xml
@ -12,6 +12,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@ -26,6 +27,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|||||||
@ -72,6 +72,12 @@
|
|||||||
"maximumError": "4kB"
|
"maximumError": "4kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
|
|||||||
54
src/main/angular/package-lock.json
generated
54
src/main/angular/package-lock.json
generated
@ -16,6 +16,8 @@
|
|||||||
"@angular/platform-browser": "^18.2.0",
|
"@angular/platform-browser": "^18.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||||
"@angular/router": "^18.2.0",
|
"@angular/router": "^18.2.0",
|
||||||
|
"@stomp/ng2-stompjs": "^8.0.0",
|
||||||
|
"ngx-cookie-service": "^18.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.14.10"
|
"zone.js": "~0.14.10"
|
||||||
@ -4283,6 +4285,39 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stomp/ng2-stompjs": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stomp/ng2-stompjs/-/ng2-stompjs-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-1TTwdK7aUaRLfewG2FOqd3ZeIyIchKwOBzJytvUckjrBbpdeAXLXY0mCBFczoY16fSsxiAnZfJffFicStD+Dug==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@stomp/rx-stomp": "^1.0.0 >=1.0.1",
|
||||||
|
"tslib": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stomp/ng2-stompjs/node_modules/tslib": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/@stomp/rx-stomp": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stomp/rx-stomp/-/rx-stomp-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-QLzPe3q0EwLB+cVWdUFEO4z5tyR+kPnXJANKN2UvB7Spz/oViHF959cydmXdQWaK7NHp86VO54TgFfXbHVnSLg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@stomp/stompjs": "^6.0.0 >=6.1.1",
|
||||||
|
"angular2-uuid": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@stomp/stompjs": {
|
||||||
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-FHDTrIFM5Ospi4L3Xhj6v2+NzCVAeNDcBe95YjUWhWiRMrBF6uN3I7AUOlRgT6jU/2WQvvYK8ZaIxFfxFp+uHQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@tufjs/canonical-json": {
|
"node_modules/@tufjs/canonical-json": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||||
@ -4943,6 +4978,12 @@
|
|||||||
"ajv": "^8.8.2"
|
"ajv": "^8.8.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/angular2-uuid": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/angular2-uuid/-/angular2-uuid-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-6AXPyii9q8KBFGagybLNVmdGJLPcVZAhmv3odNGSJIA18LuJ3xOe6uN9GvjlQsGfdmYeuxlsGnFEUu7gPhkc+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
@ -10100,6 +10141,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ngx-cookie-service": {
|
||||||
|
"version": "18.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-18.0.0.tgz",
|
||||||
|
"integrity": "sha512-hkkUckzZTXXWtFgvVkT2hg6mwYMLXioXDZWBsVCOy9gYkADjsj0N5VViO7eo2izQ0VcMPd/Etog1trf/T4oZMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^18.0.0-rc.0",
|
||||||
|
"@angular/core": "^18.0.0-rc.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nice-napi": {
|
"node_modules/nice-napi": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
||||||
|
|||||||
@ -20,7 +20,9 @@
|
|||||||
"@angular/router": "^18.2.0",
|
"@angular/router": "^18.2.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.14.10"
|
"zone.js": "~0.14.10",
|
||||||
|
"@stomp/ng2-stompjs": "^8.0.0",
|
||||||
|
"ngx-cookie-service": "^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^18.2.3",
|
"@angular-devkit/build-angular": "^18.2.3",
|
||||||
|
|||||||
20
src/main/angular/src/app/api/Session/AbstractSession.ts
Normal file
20
src/main/angular/src/app/api/Session/AbstractSession.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
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}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/angular/src/app/api/User/UserCommon.ts
Normal file
24
src/main/angular/src/app/api/User/UserCommon.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {validateListIgnoreNullItems, validateString} from "../common/validators";
|
||||||
|
import {AbstractSession} from "../Session/AbstractSession";
|
||||||
|
import {sessionFromJsonOrNull} from "../Session/sessionFromJsonOrNull";
|
||||||
|
import {UserPublic} from "./UserPublic";
|
||||||
|
|
||||||
|
export class UserCommon extends UserPublic {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
publicUuid: string,
|
||||||
|
name: string,
|
||||||
|
readonly commonSessions: AbstractSession[],
|
||||||
|
) {
|
||||||
|
super(publicUuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static override fromJson(json: any): UserCommon {
|
||||||
|
return new UserCommon(
|
||||||
|
validateString(json['publicUuid']),
|
||||||
|
validateString(json['name']),
|
||||||
|
validateListIgnoreNullItems(json['commonSessions'], sessionFromJsonOrNull),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
src/main/angular/src/app/api/User/UserPrivate.ts
Normal file
35
src/main/angular/src/app/api/User/UserPrivate.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {validateDate, validateListIgnoreNullItems, validateString} from "../common/validators";
|
||||||
|
import {AbstractSession} from "../Session/AbstractSession";
|
||||||
|
|
||||||
|
import {sessionFromJsonOrNull} from "../Session/sessionFromJsonOrNull";
|
||||||
|
|
||||||
|
export class UserPrivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly privateUuid: string,
|
||||||
|
readonly publicUuid: string,
|
||||||
|
readonly created: Date,
|
||||||
|
readonly sessions: AbstractSession[],
|
||||||
|
readonly name: string,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): UserPrivate {
|
||||||
|
return new UserPrivate(
|
||||||
|
validateString(json['privateUuid']),
|
||||||
|
validateString(json['publicUuid']),
|
||||||
|
validateDate(json['created']),
|
||||||
|
validateListIgnoreNullItems(json['sessions'], sessionFromJsonOrNull),
|
||||||
|
validateString(json['name']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJsonOrNull(json: any): UserPrivate | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return UserPrivate.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
src/main/angular/src/app/api/User/UserPublic.ts
Normal file
19
src/main/angular/src/app/api/User/UserPublic.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {validateString} from "../common/validators";
|
||||||
|
|
||||||
|
export class UserPublic {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly publicUuid: string,
|
||||||
|
readonly name: string,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): UserPublic {
|
||||||
|
return new UserPublic(
|
||||||
|
validateString(json['publicUuid']),
|
||||||
|
validateString(json['name']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
60
src/main/angular/src/app/api/User/user.service.ts
Normal file
60
src/main/angular/src/app/api/User/user.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService} from "../common/api.service";
|
||||||
|
import {UserPrivate} from "./UserPrivate";
|
||||||
|
import {Next} from "../common/types";
|
||||||
|
import {UserCommon} from "./UserCommon";
|
||||||
|
import {UserPublic} from "./UserPublic";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
import {BehaviorSubject, Subscription} from "rxjs";
|
||||||
|
import {AbstractSession} from "../Session/AbstractSession";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UserService {
|
||||||
|
|
||||||
|
private readonly subject: BehaviorSubject<UserPrivate | null> = new BehaviorSubject<UserPrivate | null>(null);
|
||||||
|
|
||||||
|
get user(): UserPrivate | null {
|
||||||
|
return this.subject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly router: Router,
|
||||||
|
protected readonly api: ApiService,
|
||||||
|
) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommonByUuid(uuid: string, next: Next<UserCommon>): void {
|
||||||
|
this.api.postSingle(['User', 'getCommonByUuid'], uuid, UserCommon.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(next?: Next<void>) {
|
||||||
|
this.api.getNone(['User', 'delete'], _ => {
|
||||||
|
this.refresh();
|
||||||
|
next && next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeName(name: string, next?: Next<UserPrivate>) {
|
||||||
|
this.api.postSingle(['User', 'changeName'], name, UserPrivate.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
goto(user: UserPublic) {
|
||||||
|
this.router.navigate(['/User', {uuid: user.publicUuid}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.api.getSingle(['User', 'whoAmI'], UserPrivate.fromJsonOrNull, user => this.subject.next(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(next: Next<UserPrivate | null>): Subscription {
|
||||||
|
return this.subject.subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
owns(session: AbstractSession): boolean {
|
||||||
|
return this.user?.publicUuid === session.owner.publicUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
4
src/main/angular/src/app/api/common/CrudDirection.ts
Normal file
4
src/main/angular/src/app/api/common/CrudDirection.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum CrudDirection {
|
||||||
|
ASC = 'ASC',
|
||||||
|
DESC = 'DESC',
|
||||||
|
}
|
||||||
13
src/main/angular/src/app/api/common/Order.ts
Normal file
13
src/main/angular/src/app/api/common/Order.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {CrudDirection} from "./CrudDirection";
|
||||||
|
|
||||||
|
export class Order<T> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly property: string,
|
||||||
|
readonly compare: (a: T, b: T) => number,
|
||||||
|
public direction: CrudDirection,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
src/main/angular/src/app/api/common/Page.ts
Normal file
28
src/main/angular/src/app/api/common/Page.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {validateList, validateNumber} from "./validators";
|
||||||
|
import {FromJson} from "./types";
|
||||||
|
|
||||||
|
export class Page<T> {
|
||||||
|
|
||||||
|
static readonly EMPTY: Page<any> = new Page(0, 0, 0, 0, []);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly size: number,
|
||||||
|
readonly number: number,
|
||||||
|
readonly totalPages: number,
|
||||||
|
readonly totalElements: number,
|
||||||
|
readonly content: T[],
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson<T>(fromJson: FromJson<T>): FromJson<Page<T>> {
|
||||||
|
return (json: any) => new Page<T>(
|
||||||
|
validateNumber(json.page.size),
|
||||||
|
validateNumber(json.page.number),
|
||||||
|
validateNumber(json.page.totalPages),
|
||||||
|
validateNumber(json.page.totalElements),
|
||||||
|
validateList(json.content, fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
src/main/angular/src/app/api/common/Sort.ts
Normal file
65
src/main/angular/src/app/api/common/Sort.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {CrudDirection} from "./CrudDirection";
|
||||||
|
|
||||||
|
import {Order} from "./Order";
|
||||||
|
|
||||||
|
export class Sort<T> {
|
||||||
|
|
||||||
|
private list: Order<T>[] = [];
|
||||||
|
|
||||||
|
toggle(key: string, compare: (a: T, b: T) => number) {
|
||||||
|
const index: number = this.indexOf(key);
|
||||||
|
if (index >= 0) {
|
||||||
|
const existing: Order<T> = this.list[index];
|
||||||
|
if (existing.direction === CrudDirection.ASC) {
|
||||||
|
existing.direction = CrudDirection.DESC;
|
||||||
|
} else {
|
||||||
|
this.list.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.list.push(new Order(key, compare, CrudDirection.ASC));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compare(a: T, b: T): number {
|
||||||
|
for (const order of this.list) {
|
||||||
|
const result = order.compare(a, b);
|
||||||
|
if (result != 0) {
|
||||||
|
return order.direction === CrudDirection.DESC ? -result : result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return this.list.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexOf(key: string): number {
|
||||||
|
return this.list.findIndex(o => o.property === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(key: string): boolean {
|
||||||
|
return this.indexOf(key) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAscending(key: string): boolean {
|
||||||
|
return this.isDirection(key, CrudDirection.ASC);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDescending(key: string): boolean {
|
||||||
|
return this.isDirection(key, CrudDirection.DESC);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirection(key: string, direction: CrudDirection): boolean {
|
||||||
|
const index: number = this.indexOf(key);
|
||||||
|
if (index < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.list[index].direction === direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
75
src/main/angular/src/app/api/common/api.service.ts
Normal file
75
src/main/angular/src/app/api/common/api.service.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {map, Subscription} from "rxjs";
|
||||||
|
import {FromJson, getApiUrl, Next} from "./types";
|
||||||
|
import {Page} from "./Page";
|
||||||
|
import {StompService} from "@stomp/ng2-stompjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
|
||||||
|
private _websocketError: boolean = false;
|
||||||
|
|
||||||
|
get websocketError(): boolean {
|
||||||
|
return this._websocketError;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly http: HttpClient,
|
||||||
|
private readonly stompService: StompService,
|
||||||
|
) {
|
||||||
|
stompService.connected$.subscribe(() => this._websocketError = false);
|
||||||
|
stompService.webSocketErrors$.subscribe(() => this._websocketError = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
connected(next: Next<void>): Subscription {
|
||||||
|
return this.stompService.connected$.subscribe(_ => next());
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe<T>(topic: any[], fromJson: FromJson<T>, next: Next<T>): Subscription {
|
||||||
|
console.info("WEBSOCKET SUBSCRIBE", topic)
|
||||||
|
return this.stompService
|
||||||
|
.subscribe(topic.join("/"))
|
||||||
|
.pipe(
|
||||||
|
map(message => message.body),
|
||||||
|
map(b => JSON.parse(b)),
|
||||||
|
map(j => fromJson(j)),
|
||||||
|
)
|
||||||
|
.subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNone<T>(path: any[], next?: Next<void>): Subscription {
|
||||||
|
return this.http.get<void>(getApiUrl('http', path), {withCredentials: true}).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(path: any[], next: Next<string>): Subscription {
|
||||||
|
return this.http.get(getApiUrl('http', path), {responseType: "text", withCredentials: true}).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSingle<T>(path: any[], fromJson: FromJson<T>, next?: Next<T>): Subscription {
|
||||||
|
return this.http.get<any>(getApiUrl('http', path), {withCredentials: true}).pipe(map(fromJson)).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
getList<T>(path: any[], fromJson: FromJson<T>, next: Next<T[]> | undefined = undefined): Subscription {
|
||||||
|
return this.http.get<any[]>(getApiUrl('http', path), {withCredentials: true}).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
postNone(path: any[], data: any, next?: Next<void>): Subscription {
|
||||||
|
return this.http.post<void>(getApiUrl('http', path), data, {withCredentials: true}).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
postSingle<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T>): Subscription {
|
||||||
|
return this.http.post<any>(getApiUrl('http', path), data, {withCredentials: true}).pipe(map(fromJson)).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
postPage<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<Page<T>> | undefined = undefined): Subscription {
|
||||||
|
return this.http.post<any>(getApiUrl('http', path), data, {withCredentials: true}).pipe(map(Page.fromJson(fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
postList<T>(path: any[], data: any, fromJson: FromJson<T>, next: Next<T[]> | undefined = undefined): Subscription {
|
||||||
|
return this.http.post<any[]>(getApiUrl('http', path), data, {withCredentials: true}).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
src/main/angular/src/app/api/common/types.ts
Normal file
9
src/main/angular/src/app/api/common/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {environment} from "../../../environments/environment";
|
||||||
|
|
||||||
|
export type FromJson<T> = (json: any) => T;
|
||||||
|
|
||||||
|
export type Next<T> = (item: T) => void;
|
||||||
|
|
||||||
|
export function getApiUrl(protocol: string, path: any[]): string {
|
||||||
|
return protocol + (environment.secure ? 's' : '') + '://' + environment.host + ':' + environment.port + '/' + environment.base + path.join('/');
|
||||||
|
}
|
||||||
73
src/main/angular/src/app/api/common/validators.ts
Normal file
73
src/main/angular/src/app/api/common/validators.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {FromJson} from "./types";
|
||||||
|
|
||||||
|
export function validateNumber(json: any): number {
|
||||||
|
if (!(typeof json === "number")) {
|
||||||
|
throw new Error("Not a number: " + json + " (" + typeof json + "): " + JSON.stringify(json));
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateNumberOrNull(json: any): number | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateNumber(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBoolean(json: any): boolean {
|
||||||
|
if (!(typeof json === "boolean")) {
|
||||||
|
throw new Error("Not a boolean: " + json + " (" + typeof json + "): " + JSON.stringify(json));
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBooleanOrNull(json: any): boolean | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateBoolean(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateString(json: any): string {
|
||||||
|
if (!(typeof json === "string")) {
|
||||||
|
throw new Error("Not a string: " + json + " (" + typeof json + "): " + JSON.stringify(json));
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateStringOrNull(json: any): string | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateString(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDate(json: any): Date {
|
||||||
|
return new Date(Date.parse(validateString(json)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDateOrNull(json: any): Date | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateDate(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateListIgnoreNullItems<T>(jsonList: any, fromJson: FromJson<T | null>): T[] {
|
||||||
|
return jsonList.map(fromJson).filter((item: T | null) => item !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateList<T>(jsonList: any, fromJson: FromJson<T>): T[] {
|
||||||
|
return jsonList.map(fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLocalDateOrNull(json: any): Date | null {
|
||||||
|
if (json === null || json === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateLocalDate(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLocalDate(json: any): Date {
|
||||||
|
return new Date(validateString(json));
|
||||||
|
}
|
||||||
17
src/main/angular/src/app/api/common/ws.ts
Normal file
17
src/main/angular/src/app/api/common/ws.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {StompService} from "@stomp/ng2-stompjs";
|
||||||
|
import {getApiUrl} from "./types";
|
||||||
|
|
||||||
|
export function stompServiceFactory() {
|
||||||
|
const stomp = new StompService({
|
||||||
|
url: getApiUrl('ws', ['ws']),
|
||||||
|
debug: false,
|
||||||
|
heartbeat_in: 2000,
|
||||||
|
heartbeat_out: 2000,
|
||||||
|
reconnect_delay: 2000,
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
stomp.connected$.subscribe(_ => console.info("WEBSOCKET CONNECTED"));
|
||||||
|
stomp.webSocketErrors$.subscribe(_ => console.info("WEBSOCKET DISCONNECTED"));
|
||||||
|
stomp.activate();
|
||||||
|
return stomp;
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
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 verteilen';
|
||||||
|
|
||||||
|
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']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
<div id="mainMenu" *ngIf="menuVisible">
|
<div id="mainMenu" *ngIf="menuVisible">
|
||||||
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/Solar">Sonnensystem</div>
|
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/SolarSystem">Sonnensystem</div>
|
||||||
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/VoltageDrop">Spannungsabfall</div>
|
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/VoltageDrop">Spannungsabfall</div>
|
||||||
|
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/Numbers">{{ NumbersSession.TYPE_DISPLAY_NAME }}</div>
|
||||||
|
<ng-container *ngIf="user !== null">
|
||||||
|
<div class="mainMenuItem mainMenuItemRight" routerLinkActive="mainMenuItemActive" routerLink="/Profile">{{ user.name }}</div>
|
||||||
|
<div class="mainMenuItem mainMenuItemRight" routerLinkActive="mainMenuItemActive" (click)="delete()">Profil löschen</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<router-outlet (activate)="onActivate($event)"/>
|
<router-outlet (activate)="onActivate($event)"/>
|
||||||
|
|||||||
@ -7,6 +7,12 @@
|
|||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mainMenuItemRight {
|
||||||
|
float: right;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mainMenuItemActive {
|
.mainMenuItemActive {
|
||||||
background-color: lightskyblue;
|
background-color: lightskyblue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
|
||||||
import {NgIf} from "@angular/common";
|
import {NgIf} from "@angular/common";
|
||||||
|
import {UserService} from "./api/User/user.service";
|
||||||
|
import {NumbersSession} from "./api/tools/Numbers/Session/NumbersSession";
|
||||||
|
import {UserPrivate} from "./api/User/UserPrivate";
|
||||||
|
import {Subscription} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -9,12 +13,42 @@ import {NgIf} from "@angular/common";
|
|||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.less'
|
styleUrl: './app.component.less'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly NumbersSession = NumbersSession;
|
||||||
|
|
||||||
|
private readonly subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
protected menuVisible = true;
|
protected menuVisible = true;
|
||||||
|
|
||||||
onActivate(component: any) {
|
protected user: UserPrivate | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly router: Router,
|
||||||
|
protected readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onActivate(component: any) {
|
||||||
this.menuVisible = !("PRINTABLE_ONLY" in component) || !component.PRINTABLE_ONLY;
|
this.menuVisible = !("PRINTABLE_ONLY" in component) || !component.PRINTABLE_ONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subscriptions.push(this.userService.subscribe(user => {
|
||||||
|
if (this.user !== null && user === null) {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
this.user = user;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
this.userService.delete();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import {routes} from './app.routes';
|
|||||||
import {registerLocaleData} from "@angular/common";
|
import {registerLocaleData} from "@angular/common";
|
||||||
import localeDe from '@angular/common/locales/de';
|
import localeDe from '@angular/common/locales/de';
|
||||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||||
|
import {provideHttpClient} from "@angular/common/http";
|
||||||
|
import {stompServiceFactory} from "./api/common/ws";
|
||||||
|
import {StompService} from "@stomp/ng2-stompjs";
|
||||||
|
|
||||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||||
|
|
||||||
@ -13,5 +16,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
{provide: LOCALE_ID, useValue: 'de-DE'},
|
{provide: LOCALE_ID, useValue: 'de-DE'},
|
||||||
provideZoneChangeDetection({eventCoalescing: true}),
|
provideZoneChangeDetection({eventCoalescing: true}),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
|
provideHttpClient(),
|
||||||
|
{provide: StompService, useFactory: stompServiceFactory},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,16 +1,28 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {SolarSystemComponent} from './solar-system/solar-system.component';
|
import {SolarSystemComponent} from './pages/tools/solar-system/solar-system.component';
|
||||||
import {VoltageDropComponent} from "./voltage-drop/voltage-drop.component";
|
import {VoltageDropComponent} from "./pages/tools/voltage-drop/voltage-drop.component";
|
||||||
import {SolarSystemPrintoutComponent} from "./solar-system/printout/solar-system-printout.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";
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: 'SolarSystemPrintout', component: SolarSystemPrintoutComponent},
|
{path: 'SolarSystemPrintout', component: SolarSystemPrintoutComponent},
|
||||||
{path: 'SolarSystem', component: SolarSystemComponent},
|
{path: 'SolarSystem', component: SolarSystemComponent},
|
||||||
|
|
||||||
{path: 'VoltageDrop', component: VoltageDropComponent},
|
{path: 'VoltageDrop', component: VoltageDropComponent},
|
||||||
|
|
||||||
|
{path: 'Numbers', component: NumbersOverviewComponent},
|
||||||
|
{path: 'Numbers/Session', component: NumbersSessionComponent},
|
||||||
|
|
||||||
|
{path: 'User', component: UserComponent},
|
||||||
|
|
||||||
|
{path: 'Profile', component: ProfileComponent},
|
||||||
|
|
||||||
// historic
|
// historic
|
||||||
{path: 'Solar', redirectTo: '/SolarSystem'},
|
{path: 'Solar', redirectTo: '/SolarSystem'},
|
||||||
|
|
||||||
// fallback
|
// fallback
|
||||||
{path: '**', redirectTo: '/Solar'},
|
{path: '**', redirectTo: '/SolarSystem'},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +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>
|
||||||
|
</ng-container>
|
||||||
29
src/main/angular/src/app/pages/profile/profile.component.ts
Normal file
29
src/main/angular/src/app/pages/profile/profile.component.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
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";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profile',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
NgIf,
|
||||||
|
FormsModule,
|
||||||
|
TextComponent,
|
||||||
|
SessionListComponent
|
||||||
|
],
|
||||||
|
templateUrl: './profile.component.html',
|
||||||
|
styleUrl: './profile.component.less'
|
||||||
|
})
|
||||||
|
export class ProfileComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
@import "../../../../tile.less";
|
||||||
|
@import "../../user/user.less";
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
@import "../numbers-common.less";
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import {Component} 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 {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly userService: UserService,
|
||||||
|
protected readonly numberSessionService: NumbersSessionService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
this.numberSessionService.create(session => this.numberSessionService.goto(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
@import "../numbers-common.less";
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Besitzer</th>
|
||||||
|
<th>Erstellt</th>
|
||||||
|
<th>Teilnehmer</th>
|
||||||
|
</tr>
|
||||||
|
<tr *ngFor="let session of sessions" (click)="numbersSessionService.goto(session)">
|
||||||
|
<td>{{ session.typeDisplayName }}</td>
|
||||||
|
<td>{{ session.title }}</td>
|
||||||
|
<td class="user" (click)="userService.goto(session.owner)">{{ session.owner.name }}</td>
|
||||||
|
<td>{{ session.created | date:'yyyy-MM-dd hh:mm' }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="user" *ngFor="let user of session.users" (click)="userService.goto(user)">
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
@import "../../numbers-common.less";
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import {BODIES, BODIES_PRINT, EARTH, EARTH_MOON, JUPITER, JUPITER_SCALED_DIAMETE
|
|||||||
import {ActivatedRoute, Params} from "@angular/router";
|
import {ActivatedRoute, Params} from "@angular/router";
|
||||||
import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
||||||
import {MIO_KILO, SolarSystemBody} from "../SolarSystemBody";
|
import {MIO_KILO, SolarSystemBody} from "../SolarSystemBody";
|
||||||
import {Unit} from "../../Unit";
|
import {Unit} from "../../../../Unit";
|
||||||
|
|
||||||
function getScale(params: Params) {
|
function getScale(params: Params) {
|
||||||
if ('scale' in params) {
|
if ('scale' in params) {
|
||||||
@ -41,12 +41,10 @@ export class SolarSystemPrintoutComponent implements OnInit {
|
|||||||
|
|
||||||
public readonly PRINTABLE_ONLY: boolean = true;
|
public readonly PRINTABLE_ONLY: boolean = true;
|
||||||
|
|
||||||
protected readonly BODIES_PRINT = BODIES_PRINT;
|
|
||||||
|
|
||||||
protected readonly SolarSystemBody = SolarSystemBody;
|
|
||||||
|
|
||||||
protected readonly makePaler = makePaler;
|
protected readonly makePaler = makePaler;
|
||||||
|
|
||||||
|
protected readonly BODIES_PRINT = BODIES_PRINT;
|
||||||
|
|
||||||
protected readonly MIO_KILO = MIO_KILO;
|
protected readonly MIO_KILO = MIO_KILO;
|
||||||
|
|
||||||
protected readonly EARTH = EARTH;
|
protected readonly EARTH = EARTH;
|
||||||
@ -1,13 +1,4 @@
|
|||||||
@import "../../tile.less";
|
@import "../../../../tile.less";
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0.2em solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -3,7 +3,7 @@ import {DecimalPipe, NgForOf} from "@angular/common";
|
|||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {MIO_KILO, SolarSystemBody} from "./SolarSystemBody";
|
import {MIO_KILO, SolarSystemBody} from "./SolarSystemBody";
|
||||||
import {BODIES, JUPITER, JUPITER_SCALED_DIAMETER} from "./SOLAR_SYSTEM";
|
import {BODIES, JUPITER, JUPITER_SCALED_DIAMETER} from "./SOLAR_SYSTEM";
|
||||||
import {applyPrefixUnit, Unit} from "../Unit";
|
import {applyPrefixUnit, Unit} from "../../../Unit";
|
||||||
import {RouterLink} from "@angular/router";
|
import {RouterLink} from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -1,4 +1,4 @@
|
|||||||
@import "../../tile.less";
|
@import "../../../../tile.less";
|
||||||
|
|
||||||
#VoltageDropInputs {
|
#VoltageDropInputs {
|
||||||
|
|
||||||
@ -18,14 +18,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shortcuts {
|
.shortcuts {
|
||||||
padding-top: calc(@space / 2);
|
padding-top: @halfSpace;
|
||||||
padding-bottom: calc(@space * 2);
|
padding-bottom: calc(@space * 2);
|
||||||
|
|
||||||
.shortcut {
|
.shortcut {
|
||||||
float: left;
|
float: left;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
padding: calc(@space / 2) @space;
|
padding: @halfSpace @space;
|
||||||
margin-right: calc(@space / 2);
|
margin-right: @halfSpace;
|
||||||
background-color: lightskyblue;
|
background-color: lightskyblue;
|
||||||
border-radius: @space;
|
border-radius: @space;
|
||||||
}
|
}
|
||||||
4
src/main/angular/src/app/pages/user/user.component.html
Normal file
4
src/main/angular/src/app/pages/user/user.component.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<ng-container *ngIf="user !== null">
|
||||||
|
<h2>Benutzer: {{ user.name }}</h2>
|
||||||
|
<app-session-list [sessions]="user.commonSessions"></app-session-list>
|
||||||
|
</ng-container>
|
||||||
39
src/main/angular/src/app/pages/user/user.component.ts
Normal file
39
src/main/angular/src/app/pages/user/user.component.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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 {UserCommon} from "../../api/User/UserCommon";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
NgIf,
|
||||||
|
SessionListComponent
|
||||||
|
],
|
||||||
|
templateUrl: './user.component.html',
|
||||||
|
styleUrl: './user.component.less'
|
||||||
|
})
|
||||||
|
export class UserComponent implements OnInit {
|
||||||
|
|
||||||
|
protected user: UserCommon | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly activatedRoute: ActivatedRoute,
|
||||||
|
protected readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.activatedRoute.params.subscribe(params => {
|
||||||
|
const uuid = params['uuid'];
|
||||||
|
if (uuid) {
|
||||||
|
this.userService.getCommonByUuid(uuid, user => this.user = user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
src/main/angular/src/app/pages/user/user.less
Normal file
10
src/main/angular/src/app/pages/user/user.less
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.user {
|
||||||
|
float: left;
|
||||||
|
padding: @halfSpace;
|
||||||
|
background-color: lightskyblue;
|
||||||
|
border-radius: @halfSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user:hover {
|
||||||
|
background-color: dodgerblue;
|
||||||
|
}
|
||||||
1
src/main/angular/src/app/shared/text/text.component.html
Normal file
1
src/main/angular/src/app/shared/text/text.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<input [(ngModel)]="model" (focus)="begin()" (blur)="apply()" (keydown.enter)="apply()" (keydown.escape)="abort()">
|
||||||
56
src/main/angular/src/app/shared/text/text.component.ts
Normal file
56
src/main/angular/src/app/shared/text/text.component.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-text',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './text.component.html',
|
||||||
|
styleUrl: './text.component.less'
|
||||||
|
})
|
||||||
|
export class TextComponent implements OnInit {
|
||||||
|
|
||||||
|
private _initial: string = "";
|
||||||
|
|
||||||
|
protected model: string = this._initial;
|
||||||
|
|
||||||
|
protected editing: boolean = false;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
readonly onChange = new EventEmitter<string>();
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set initial(value: string) {
|
||||||
|
this._initial = value;
|
||||||
|
if (!this.editing) {
|
||||||
|
this.model = this._initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.model = this._initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
begin() {
|
||||||
|
this.editing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
if (this.model !== this._initial) {
|
||||||
|
this.onChange.emit(this.model);
|
||||||
|
}
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this.model = this._initial;
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
this.editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
@space: 0.5em;
|
@space: 0.5em;
|
||||||
|
@halfSpace: calc(@space / 2);
|
||||||
|
|||||||
7
src/main/angular/src/environments/environment.prod.ts
Normal file
7
src/main/angular/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
host: window.location.host.split(":")[0],
|
||||||
|
port: window.location.port,
|
||||||
|
base: 'Data/',
|
||||||
|
secure: window.location.protocol === "https:",
|
||||||
|
};
|
||||||
8
src/main/angular/src/environments/environment.ts
Normal file
8
src/main/angular/src/environments/environment.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
host: window.location.host.split(":")[0],
|
||||||
|
port: 8080,
|
||||||
|
base: '',
|
||||||
|
secure: window.location.protocol === "https:",
|
||||||
|
};
|
||||||
|
|
||||||
@ -1,11 +1,11 @@
|
|||||||
@import "config.less";
|
@import "config.less";
|
||||||
|
|
||||||
.tileContainer {
|
.tileContainer {
|
||||||
padding: calc(@space / 2);
|
padding: @halfSpace;
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: calc(@space / 2);
|
padding: @halfSpace;
|
||||||
|
|
||||||
.tileInner {
|
.tileInner {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@ -14,12 +14,22 @@
|
|||||||
|
|
||||||
.tileTitle {
|
.tileTitle {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: calc(@space / 2) @space;
|
padding: @halfSpace @space;
|
||||||
background-color: lightskyblue;
|
background-color: lightskyblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tileContent {
|
.tileContent {
|
||||||
padding: calc(@space / 2);
|
padding: @halfSpace;
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0.2em solid white;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
src/main/java/de/ph87/tools/UserArgumentResolver.java
Normal file
40
src/main/java/de/ph87/tools/UserArgumentResolver.java
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package de.ph87.tools;
|
||||||
|
|
||||||
|
import de.ph87.tools.user.User;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||||
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
|
||||||
|
|
||||||
|
public static final String USER_UUID_COOKIE_NAME = "PatrixToolsUserUuid";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsParameter(@NonNull final MethodParameter parameter) {
|
||||||
|
return parameter.getParameterType() == User.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User resolveArgument(@NonNull final MethodParameter parameter, final ModelAndViewContainer mavContainer, @NonNull final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
|
||||||
|
if (!(webRequest instanceof final HttpServletRequest request)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String uuid = Arrays.stream(request.getCookies()).filter(cookie -> USER_UUID_COOKIE_NAME.equalsIgnoreCase(cookie.getName())).findFirst().map(Cookie::getValue).orElse(null);
|
||||||
|
if (uuid == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
78
src/main/java/de/ph87/tools/session/AbstractSession.java
Normal file
78
src/main/java/de/ph87/tools/session/AbstractSession.java
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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;
|
||||||
|
this.join(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void join(@NonNull final User user) {
|
||||||
|
synchronized (uuid) {
|
||||||
|
users.add(user);
|
||||||
|
touch();
|
||||||
|
user.join(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void leave(@NonNull final User user) {
|
||||||
|
synchronized (uuid) {
|
||||||
|
users.remove(user);
|
||||||
|
touch();
|
||||||
|
user.leave(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void touch() {
|
||||||
|
lastAccess = ZonedDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Object> getWebsocketTopic() {
|
||||||
|
return List.of("Number", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
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(1);
|
||||||
|
synchronized (sessions) {
|
||||||
|
sessions.removeIf(session -> session.getLastAccess().isBefore(deadline));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationEventPublisher.publishEvent(session);
|
||||||
|
applicationEventPublisher.publishEvent(user);
|
||||||
|
return toDto(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);
|
||||||
|
}
|
||||||
|
session.join(user);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
60
src/main/java/de/ph87/tools/session/AbstractSessionDto.java
Normal file
60
src/main/java/de/ph87/tools/session/AbstractSessionDto.java
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package de.ph87.tools.session;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SessionChangePasswordInbound {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String uuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String password;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package de.ph87.tools.session;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SessionChangeTitleInbound {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String uuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String title;
|
||||||
|
|
||||||
|
}
|
||||||
19
src/main/java/de/ph87/tools/session/SessionJoinInbound.java
Normal file
19
src/main/java/de/ph87/tools/session/SessionJoinInbound.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package de.ph87.tools.session;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SessionJoinInbound {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String uuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String password;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
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 org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
62
src/main/java/de/ph87/tools/user/User.java
Normal file
62
src/main/java/de/ph87/tools/user/User.java
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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 java.time.ZonedDateTime;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class User implements IWebSocketMessage {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String privateUuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String publicUuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
public final ZonedDateTime created = ZonedDateTime.now();
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ToString.Exclude
|
||||||
|
private final Set<AbstractSession> sessions = new HashSet<>();
|
||||||
|
|
||||||
|
private ZonedDateTime lastAccess = created;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
public String name = "unnamed";
|
||||||
|
|
||||||
|
private 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 (privateUuid) {
|
||||||
|
sessions.remove(session);
|
||||||
|
touch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Object> getWebsocketTopic() {
|
||||||
|
return List.of("User", privateUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
src/main/java/de/ph87/tools/user/UserCommonDto.java
Normal file
34
src/main/java/de/ph87/tools/user/UserCommonDto.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package de.ph87.tools.user;
|
||||||
|
|
||||||
|
import de.ph87.tools.session.AbstractSessionDto;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class UserCommonDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String publicUuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Set<AbstractSessionDto> commonSessions;
|
||||||
|
|
||||||
|
public UserCommonDto(@NonNull final User target, @Nullable final User principal) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
src/main/java/de/ph87/tools/user/UserController.java
Normal file
44
src/main/java/de/ph87/tools/user/UserController.java
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package de.ph87.tools.user;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import static de.ph87.tools.UserArgumentResolver.USER_UUID_COOKIE_NAME;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("User")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@PostMapping("changeName")
|
||||||
|
public UserPrivateDto changeName(@CookieValue(name = USER_UUID_COOKIE_NAME) @NonNull final String userUuid, @NonNull @RequestBody final String name) {
|
||||||
|
return userService.changeName(userUuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@PostMapping("getCommonByUuid")
|
||||||
|
public UserCommonDto getCommonByUuid(@CookieValue(name = USER_UUID_COOKIE_NAME, required = false) @Nullable final String userUuid, @NonNull @RequestBody final String targetUuid) {
|
||||||
|
return userService.getCommonByUuid(userUuid, targetUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
src/main/java/de/ph87/tools/user/UserPrivateDto.java
Normal file
48
src/main/java/de/ph87/tools/user/UserPrivateDto.java
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package de.ph87.tools.user;
|
||||||
|
|
||||||
|
import de.ph87.tools.session.AbstractSessionDto;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
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 UserPrivateDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String publicUuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String privateUuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ZonedDateTime created;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Set<AbstractSessionDto> sessions;
|
||||||
|
|
||||||
|
public UserPrivateDto(@NonNull final User user) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/java/de/ph87/tools/user/UserPublicDto.java
Normal file
30
src/main/java/de/ph87/tools/user/UserPublicDto.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package de.ph87.tools.user;
|
||||||
|
|
||||||
|
import de.ph87.tools.web.IWebSocketMessage;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class UserPublicDto implements IWebSocketMessage {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String publicUuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
public UserPublicDto(@NonNull final User user) {
|
||||||
|
this.publicUuid = user.getPublicUuid();
|
||||||
|
this.name = user.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Object> getWebsocketTopic() {
|
||||||
|
return List.of("User", publicUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
115
src/main/java/de/ph87/tools/user/UserService.java
Normal file
115
src/main/java/de/ph87/tools/user/UserService.java
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package de.ph87.tools.user;
|
||||||
|
|
||||||
|
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.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static de.ph87.tools.UserArgumentResolver.USER_UUID_COOKIE_NAME;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@EnableScheduling
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
private final Set<User> users = new HashSet<>();
|
||||||
|
|
||||||
|
@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);
|
||||||
|
writeUserUuidCookie(response, user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public UserPrivateDto getUserByUuidOrNull(@Nullable final String userUuid, final @NonNull HttpServletResponse response) {
|
||||||
|
if (userUuid == null || userUuid.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final User user = findByPrivateUuid(userUuid).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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public User getByPrivateUuidOrThrow(@NonNull final String uuid) {
|
||||||
|
synchronized (users) {
|
||||||
|
return findByPrivateUuid(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public User getByPublicUuid(@NonNull final String uuid) {
|
||||||
|
synchronized (users) {
|
||||||
|
return findByPublicUuid(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Optional<User> findByPrivateUuid(@NonNull final String uuid) {
|
||||||
|
return users.stream().filter(u -> u.getPrivateUuid().equals(uuid)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Optional<User> findByPublicUuid(@NonNull final String uuid) {
|
||||||
|
return users.stream().filter(u -> u.getPublicUuid().equals(uuid)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private User create() {
|
||||||
|
final User user = new User();
|
||||||
|
users.add(user);
|
||||||
|
log.info("User CREATED: {}", user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeUserUuidCookie(@NonNull final HttpServletResponse response, @Nullable final User user) {
|
||||||
|
final Cookie userUuidCookie = new Cookie(USER_UUID_COOKIE_NAME, "");
|
||||||
|
if (user != null) {
|
||||||
|
userUuidCookie.setValue(user.privateUuid);
|
||||||
|
}
|
||||||
|
userUuidCookie.setPath("/");
|
||||||
|
response.addCookie(userUuidCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
src/main/java/de/ph87/tools/web/IWebSocketMessage.java
Normal file
12
src/main/java/de/ph87/tools/web/IWebSocketMessage.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package de.ph87.tools.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IWebSocketMessage {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
List<Object> getWebsocketTopic();
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,24 +1,32 @@
|
|||||||
package de.ph87.tools.web;
|
package de.ph87.tools.web;
|
||||||
|
|
||||||
|
import de.ph87.tools.UserArgumentResolver;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||||
|
argumentResolvers.add(new UserArgumentResolver());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*");
|
registry.addMapping("/**").allowCredentials(true).allowedOriginPatterns("*").allowedMethods("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
35
src/main/java/de/ph87/tools/web/WebSocketConfig.java
Normal file
35
src/main/java/de/ph87/tools/web/WebSocketConfig.java
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package de.ph87.tools.web;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||||
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
|
@CrossOrigin
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
public static final String DESTINATION = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
registry.addEndpoint("/ws").setAllowedOrigins("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
|
config.enableSimpleBroker(DESTINATION).setHeartbeatValue(new long[]{2000, 2000}).setTaskScheduler(heartBeatScheduler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TaskScheduler heartBeatScheduler() {
|
||||||
|
return new ThreadPoolTaskScheduler();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
25
src/main/java/de/ph87/tools/web/WebSocketService.java
Normal file
25
src/main/java/de/ph87/tools/web/WebSocketService.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package de.ph87.tools.web;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSocketService {
|
||||||
|
|
||||||
|
private final SimpMessageSendingOperations simpMessageSendingOperations;
|
||||||
|
|
||||||
|
@EventListener(IWebSocketMessage.class)
|
||||||
|
public void send(@NonNull final IWebSocketMessage message) {
|
||||||
|
final String topic = message.getWebsocketTopic().stream().map(Object::toString).collect(Collectors.joining("/"));
|
||||||
|
simpMessageSendingOperations.convertAndSend(topic, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user