Compare commits
5 Commits
af43204063
...
f7af71d52b
| Author | SHA1 | Date | |
|---|---|---|---|
| f7af71d52b | |||
| 39456f83d0 | |||
| 530dcd2f04 | |||
| ac82b8a0ac | |||
| 43208bf37d |
@ -6,3 +6,8 @@ spring.datasource.username=sa
|
|||||||
spring.datasource.password=password
|
spring.datasource.password=password
|
||||||
#-
|
#-
|
||||||
#spring.jpa.hibernate.ddl-auto=create
|
#spring.jpa.hibernate.ddl-auto=create
|
||||||
|
#-
|
||||||
|
de.ph87.tools.email.host=mail.ph87.de
|
||||||
|
de.ph87.tools.email.username=_noreply_@ph87.de
|
||||||
|
de.ph87.tools.email.password=zOt4EmTTTGdjcNtb5hfN
|
||||||
|
de.ph87.tools.email.from=_noreply_@ph87.de
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@ -50,6 +50,12 @@
|
|||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.sun.mail</groupId>
|
||||||
|
<artifactId>jakarta.mail</artifactId>
|
||||||
|
<version>2.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|||||||
@ -9,6 +9,8 @@ export class UserPrivate extends UserPublic {
|
|||||||
readonly created: Date,
|
readonly created: Date,
|
||||||
name: string,
|
name: string,
|
||||||
readonly password: boolean,
|
readonly password: boolean,
|
||||||
|
readonly email: string,
|
||||||
|
readonly emailConfirmed: boolean,
|
||||||
admin: boolean,
|
admin: boolean,
|
||||||
) {
|
) {
|
||||||
super(publicUuid, name, admin);
|
super(publicUuid, name, admin);
|
||||||
@ -21,6 +23,8 @@ export class UserPrivate extends UserPublic {
|
|||||||
validateDate(json['created']),
|
validateDate(json['created']),
|
||||||
validateString(json['name']),
|
validateString(json['name']),
|
||||||
validateBoolean(json['password']),
|
validateBoolean(json['password']),
|
||||||
|
validateString(json['email']),
|
||||||
|
validateBoolean(json['emailConfirmed']),
|
||||||
validateBoolean(json['admin']),
|
validateBoolean(json['admin']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
export class UserLoginRequest {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly username: string,
|
||||||
|
readonly password: string,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ import {Numbers} from "../tools/Numbers/Numbers";
|
|||||||
import {GroupDeletedEvent} from "../group/events/GroupDeletedEvent";
|
import {GroupDeletedEvent} from "../group/events/GroupDeletedEvent";
|
||||||
import {GroupLeftEvent} from "../group/events/GroupLeftEvent";
|
import {GroupLeftEvent} from "../group/events/GroupLeftEvent";
|
||||||
import {UserLogoutEvent} from "./events/UserLogoutEvent";
|
import {UserLogoutEvent} from "./events/UserLogoutEvent";
|
||||||
|
import {validateBoolean} from "../common/validators";
|
||||||
|
import {UserLoginRequest} from "./requests/UserLoginRequest";
|
||||||
|
|
||||||
function userPushMessageFromJson(json: any): object {
|
function userPushMessageFromJson(json: any): object {
|
||||||
const type = json['_type_'];
|
const type = json['_type_'];
|
||||||
@ -58,8 +60,23 @@ export class UserService {
|
|||||||
this.subscribePush(UserLogoutEvent, _ => this.setUser(null));
|
this.subscribePush(UserLogoutEvent, _ => this.setUser(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchUser() {
|
public fetchUser() {
|
||||||
this.api.getSingle(['User', 'whoAmI'], UserPrivate.fromJson, user => this.setUser(user));
|
this.api.getSingle(['User', 'whoAmI'], UserPrivate.fromJsonOrNull, user => this.setUser(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
login(username: string, password: string) {
|
||||||
|
const data = new UserLoginRequest(username, password);
|
||||||
|
this.api.postSingle(['User', 'login'], data, UserPrivate.fromJson, user => {
|
||||||
|
this.setUser(user);
|
||||||
|
this.gotoProfile();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.api.getNone(['User', 'logout'], () => {
|
||||||
|
this.setUser(null);
|
||||||
|
this.gotoLogin();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUser(user: UserPrivate | null) {
|
private setUser(user: UserPrivate | null) {
|
||||||
@ -89,6 +106,14 @@ export class UserService {
|
|||||||
this.api.postSingle(['User', 'changePassword'], password, UserPrivate.fromJson, next);
|
this.api.postSingle(['User', 'changePassword'], password, UserPrivate.fromJson, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeEmail(email: string, next?: Next<UserPrivate>) {
|
||||||
|
this.api.postSingle(['User', 'changeEmail'], email, UserPrivate.fromJson, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmEmail(emailConfirmation: string, next?: Next<boolean>) {
|
||||||
|
this.api.postSingle(['User', 'confirmEmail'], emailConfirmation, validateBoolean, next);
|
||||||
|
}
|
||||||
|
|
||||||
goto(user: UserPublic) {
|
goto(user: UserPublic) {
|
||||||
this.router.navigate(['User', user.publicUuid]);
|
this.router.navigate(['User', user.publicUuid]);
|
||||||
}
|
}
|
||||||
@ -109,4 +134,13 @@ export class UserService {
|
|||||||
)
|
)
|
||||||
.subscribe(next);
|
.subscribe(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gotoLogin() {
|
||||||
|
this.router.navigate(['Login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
gotoProfile() {
|
||||||
|
this.router.navigate(['Profile']);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,10 @@ export class GroupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(next: Next<Group>): void {
|
create(next: Next<Group>): void {
|
||||||
this.api.getSingle(['Group', 'create'], Group.fromJson, next);
|
this.api.getSingle(['Group', 'create'], Group.fromJson, group => {
|
||||||
|
this.userService.fetchUser(); // to update userService with possible freshly created user
|
||||||
|
next(group);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTitle(group: Group, title: string, next?: Next<Group>): void {
|
changeTitle(group: Group, title: string, next?: Next<Group>): void {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/SolarSystem">Planeten</div>
|
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/SolarSystem">Planeten</div>
|
||||||
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/VoltageDrop">Kabel</div>
|
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/VoltageDrop">Kabel</div>
|
||||||
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/Groups">Gruppen</div>
|
<div class="mainMenuItem" routerLinkActive="mainMenuItemActive" routerLink="/Groups">Gruppen</div>
|
||||||
|
|
||||||
<ng-container *ngIf="userService.user !== null">
|
<ng-container *ngIf="userService.user !== null">
|
||||||
<div
|
<div
|
||||||
class="mainMenuItem mainMenuItemRight"
|
class="mainMenuItem mainMenuItemRight"
|
||||||
@ -13,5 +14,12 @@
|
|||||||
{{ userService.user.name }}
|
{{ userService.user.name }}
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="userService.user === null">
|
||||||
|
<div class="mainMenuItem mainMenuItemRight" routerLinkActive="mainMenuItemActive" routerLink="/Login">
|
||||||
|
Login
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<router-outlet (activate)="onActivate($event)"/>
|
<router-outlet (activate)="onActivate($event)"/>
|
||||||
|
|||||||
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
#mainMenu {
|
#mainMenu {
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
|
margin-top: -1px;
|
||||||
|
|
||||||
.mainMenuItem {
|
.mainMenuItem {
|
||||||
float: left;
|
float: left;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainMenuItemRight {
|
.mainMenuItemRight {
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import {UserComponent} from "./pages/user/user.component";
|
|||||||
import {GroupsComponent} from "./pages/group/groups/groups.component";
|
import {GroupsComponent} from "./pages/group/groups/groups.component";
|
||||||
import {GroupComponent} from "./pages/group/group/group.component";
|
import {GroupComponent} from "./pages/group/group/group.component";
|
||||||
import {NumbersComponent} from "./pages/tools/numbers/numbers.component";
|
import {NumbersComponent} from "./pages/tools/numbers/numbers.component";
|
||||||
|
import {EmailConfirmationComponent} from "./pages/email-confirmation/email-confirmation.component";
|
||||||
|
import {LoginComponent} from "./pages/login/login.component";
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: 'SolarSystemPrintout', component: SolarSystemPrintoutComponent},
|
{path: 'SolarSystemPrintout', component: SolarSystemPrintoutComponent},
|
||||||
@ -21,7 +23,9 @@ export const routes: Routes = [
|
|||||||
|
|
||||||
{path: 'User/:publicUuid', component: UserComponent},
|
{path: 'User/:publicUuid', component: UserComponent},
|
||||||
|
|
||||||
|
{path: 'Login', component: LoginComponent},
|
||||||
{path: 'Profile', component: ProfileComponent},
|
{path: 'Profile', component: ProfileComponent},
|
||||||
|
{path: 'emailConfirmation/:emailConfirmation', component: EmailConfirmationComponent},
|
||||||
|
|
||||||
// fallback
|
// fallback
|
||||||
{path: '**', redirectTo: '/SolarSystem'},
|
{path: '**', redirectTo: '/SolarSystem'},
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
<div *ngIf="confirmed === null" class="emailConfirmation emailConfirmationPending">
|
||||||
|
Bestätigung läuft...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="confirmed === false" class="emailConfirmation emailConfirmationFailed">
|
||||||
|
Bestätigung fehlgeschlagen
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="confirmed === true" class="emailConfirmation emailConfirmationSuccess">
|
||||||
|
Bestätigung erfolgreich
|
||||||
|
</div>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
.emailConfirmation {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1em;
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailConfirmationPending {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailConfirmationFailed {
|
||||||
|
color: indianred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailConfirmationSuccess {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {Subscription} from "rxjs";
|
||||||
|
import {ActivatedRoute, Router} from "@angular/router";
|
||||||
|
import {GroupService} from "../../api/group/group.service";
|
||||||
|
import {UserService} from "../../api/User/user.service";
|
||||||
|
import {NgIf} from "@angular/common";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-email-confirmation',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './email-confirmation.component.html',
|
||||||
|
styleUrl: './email-confirmation.component.less'
|
||||||
|
})
|
||||||
|
export class EmailConfirmationComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected confirmed: boolean | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly router: Router,
|
||||||
|
protected readonly activatedRoute: ActivatedRoute,
|
||||||
|
protected readonly groupService: GroupService,
|
||||||
|
protected readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.activatedRoute.params.subscribe(params => {
|
||||||
|
const emailConfirmation = params['emailConfirmation'];
|
||||||
|
if (emailConfirmation) {
|
||||||
|
this.userService.confirmEmail(emailConfirmation, confirmed => {
|
||||||
|
this.confirmed = confirmed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
this.subs.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/angular/src/app/pages/login/login.component.html
Normal file
24
src/main/angular/src/app/pages/login/login.component.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<div class="tileContainer">
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tileInner">
|
||||||
|
<div class="tileTitle">
|
||||||
|
Login
|
||||||
|
</div>
|
||||||
|
<div class="tileContent">
|
||||||
|
|
||||||
|
<div>Benutzername</div>
|
||||||
|
<input type="text" [(ngModel)]="username" (keydown.enter)="login()">
|
||||||
|
|
||||||
|
<div>Passwort</div>
|
||||||
|
<input type="password" [(ngModel)]="password" (keydown.enter)="login()">
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button" type="password" [(ngModel)]="password" (click)="login()">Login</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
48
src/main/angular/src/app/pages/login/login.component.ts
Normal file
48
src/main/angular/src/app/pages/login/login.component.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {UserService} from "../../api/User/user.service";
|
||||||
|
import {Subscription} from "rxjs";
|
||||||
|
import {UserPrivate} from "../../api/User/UserPrivate";
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.less'
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private readonly subs: Subscription[] = [];
|
||||||
|
|
||||||
|
protected username: string = '';
|
||||||
|
|
||||||
|
protected password: string = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
// -
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.userService.subscribePush(UserPrivate, user => {
|
||||||
|
if (user !== null) {
|
||||||
|
this.userService.gotoProfile();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe());
|
||||||
|
this.subs.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
this.userService.login(this.username, this.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly alert = alert;
|
||||||
|
}
|
||||||
@ -32,6 +32,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tileInner">
|
||||||
|
<div class="tileTitle">
|
||||||
|
E-Mail
|
||||||
|
<span *ngIf="userService.user.email === ''" class="passwordNotSet">(nicht gesetzt))</span>
|
||||||
|
<span *ngIf="userService.user.email !== '' && !userService.user.emailConfirmed" class="passwordNotSet">(nicht bestätigt))</span>
|
||||||
|
<span *ngIf="userService.user.email !== '' && userService.user.emailConfirmed" class="passwordSet">(bestätigt)</span>
|
||||||
|
</div>
|
||||||
|
<div class="tileContent">
|
||||||
|
<app-text initial="" [placeholder]="userService.user.email" [editable]="true" (onChange)="userService.changeEmail($event)" [validator]="emailValidator"></app-text>
|
||||||
|
<app-text initial="" placeholder="Bestätigungscode eingeben..." [editable]="true" (onChange)="userService.confirmEmail($event)" *ngIf="userService.user.email !== '' && !userService.user.emailConfirmed"></app-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tileInner">
|
||||||
|
<div class="tileTitle">
|
||||||
|
Ausloggen
|
||||||
|
</div>
|
||||||
|
<div class="tileContent">
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button buttonLogout" (click)="logout()">
|
||||||
|
Ausloggen
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
import {Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||||
import {NgForOf, NgIf} from "@angular/common";
|
import {NgForOf, NgIf} from "@angular/common";
|
||||||
import {UserService} from "../../api/User/user.service";
|
import {UserService} from "../../api/User/user.service";
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {TextComponent} from "../../shared/text/text.component";
|
import {TextComponent} from "../../shared/text/text.component";
|
||||||
import {GroupListComponent} from "../group/shared/group-list/group-list.component";
|
import {GroupListComponent} from "../group/shared/group-list/group-list.component";
|
||||||
import {Group} from "../../api/group/Group";
|
|
||||||
import {Subscription} from "rxjs";
|
|
||||||
import {GroupService} from "../../api/group/group.service";
|
import {GroupService} from "../../api/group/group.service";
|
||||||
import {UserPrivate} from "../../api/User/UserPrivate";
|
|
||||||
|
|
||||||
const USER_NAME_MIN_LENGTH = 2;
|
const USER_NAME_MIN_LENGTH = 2;
|
||||||
|
|
||||||
@ -26,19 +23,17 @@ const USER_PASSWORD_MIN_LENGTH = 10;
|
|||||||
templateUrl: './profile.component.html',
|
templateUrl: './profile.component.html',
|
||||||
styleUrl: './profile.component.less'
|
styleUrl: './profile.component.less'
|
||||||
})
|
})
|
||||||
export class ProfileComponent implements OnInit, OnDestroy {
|
export class ProfileComponent {
|
||||||
|
|
||||||
protected readonly USER_NAME_MIN_LENGTH = USER_NAME_MIN_LENGTH;
|
protected readonly USER_NAME_MIN_LENGTH = USER_NAME_MIN_LENGTH;
|
||||||
|
|
||||||
protected readonly USER_PASSWORD_MIN_LENGTH = USER_PASSWORD_MIN_LENGTH;
|
protected readonly USER_PASSWORD_MIN_LENGTH = USER_PASSWORD_MIN_LENGTH;
|
||||||
|
|
||||||
private readonly subs: Subscription[] = [];
|
|
||||||
|
|
||||||
protected password0: string = "";
|
protected password0: string = "";
|
||||||
|
|
||||||
protected password1: string = "";
|
protected password1: string = "";
|
||||||
|
|
||||||
protected groups: Group[] = [];
|
protected email: string = "";
|
||||||
|
|
||||||
@ViewChild('p1')
|
@ViewChild('p1')
|
||||||
p1!: ElementRef;
|
p1!: ElementRef;
|
||||||
@ -50,22 +45,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
// -
|
// -
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.updateGroupList();
|
|
||||||
this.subs.push(this.userService.subscribePush(UserPrivate, _ => {
|
|
||||||
this.updateGroupList();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subs.forEach(sub => sub.unsubscribe());
|
|
||||||
this.subs.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateGroupList() {
|
|
||||||
this.groupService.findAllJoined(groups => this.groups = groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected nameValidator(name: string): boolean {
|
protected nameValidator(name: string): boolean {
|
||||||
return name.length >= USER_NAME_MIN_LENGTH && !/\s+|^[^a-zA-Z0-9]+$/.test(name);
|
return name.length >= USER_NAME_MIN_LENGTH && !/\s+|^[^a-zA-Z0-9]+$/.test(name);
|
||||||
}
|
}
|
||||||
@ -74,6 +53,10 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
return password.length >= USER_PASSWORD_MIN_LENGTH && !/^[a-zA-Z]+$|^[0-9]+$/.test(password);
|
return password.length >= USER_PASSWORD_MIN_LENGTH && !/^[a-zA-Z]+$|^[0-9]+$/.test(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected emailValidator(email: string) {
|
||||||
|
return /^\w+([-.]\w+)*@\w+([-.]\w+)*\.\w{2,}$/.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
protected p0Invalid(): boolean {
|
protected p0Invalid(): boolean {
|
||||||
return this.password0 !== '' && !this.passwordValidator(this.password0);
|
return this.password0 !== '' && !this.passwordValidator(this.password0);
|
||||||
}
|
}
|
||||||
@ -111,4 +94,16 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
if (!this.userService.user?.password) {
|
||||||
|
if (!confirm("Du hast kein Passwort gesetzt. Du wirst Dich nicht wieder einloggen können. Trotzdem abmelden?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm("Wirklich sicher?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.userService.logout()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
<input
|
<input
|
||||||
*ngIf="editable"
|
*ngIf="editable"
|
||||||
[(ngModel)]="model"
|
[(ngModel)]="model"
|
||||||
|
[placeholder]="_placeholder"
|
||||||
(focus)="begin()"
|
(focus)="begin()"
|
||||||
(blur)="apply()"
|
(blur)="apply()"
|
||||||
(keydown.enter)="apply()"
|
(keydown.enter)="apply()"
|
||||||
(keydown.escape)="abort()"
|
(keydown.escape)="abort()"
|
||||||
[class.invalid]="validator !== null && !validator(model)"
|
[class.invalid]="model !== _initial && validator !== null && !validator(model)"
|
||||||
[class.unsaved]="model !== _initial"
|
[class.unsaved]="model !== _initial"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,18 @@ export class TextComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
set initial(value: string) {
|
set initial(value: string) {
|
||||||
this._initial = value;
|
this._initial = value;
|
||||||
|
this.updateFromOutside();
|
||||||
|
}
|
||||||
|
|
||||||
|
_placeholder: string = '';
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set placeholder(value: string) {
|
||||||
|
this._placeholder = value;
|
||||||
|
this.updateFromOutside();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFromOutside() {
|
||||||
if (!this.editing) {
|
if (!this.editing) {
|
||||||
this.model = this._initial;
|
this.model = this._initial;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
float: left;
|
float: left;
|
||||||
|
text-align: center;
|
||||||
margin-left: @quarterSpace;
|
margin-left: @quarterSpace;
|
||||||
margin-right: @quarterSpace;
|
margin-right: @quarterSpace;
|
||||||
padding: @halfSpace;
|
padding: @halfSpace;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public class EmailHelper {
|
public class EmailHelper {
|
||||||
|
|
||||||
private static final Pattern REGEX = Pattern.compile("(?<username>(?<usernameFirst>[^@])[^@]*)@(?<domain>(?<domainFirst>[^.]).*(?<tld>\\.[^.]+))");
|
private static final Pattern REGEX = Pattern.compile("(?<username>\\w+(?:[-.]\\w+)*)@(?<domain>\\w+(?:[-.]\\w+)*)\\.(?<tld>\\w{2,})");
|
||||||
|
|
||||||
public static boolean isEmailValid(@NonNull final String email) {
|
public static boolean isEmailValid(@NonNull final String email) {
|
||||||
return REGEX.matcher(email).matches();
|
return REGEX.matcher(email).matches();
|
||||||
@ -19,10 +19,10 @@ public class EmailHelper {
|
|||||||
if (!matcher.find()) {
|
if (!matcher.find()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
final String usernameFirst = matcher.group("usernameFirst");
|
final String usernameFirst = matcher.group("username");
|
||||||
final String domainFirst = matcher.group("domainFirst");
|
final String domainFirst = matcher.group("domain");
|
||||||
final String tld = matcher.group("tld");
|
final String tld = matcher.group("tld");
|
||||||
return "%s...@%s...%s".formatted(usernameFirst, domainFirst, tld);
|
return "%s...@%s...%s".formatted(usernameFirst.charAt(0), domainFirst.charAt(0), tld);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/main/java/de/ph87/tools/email/Email.java
Normal file
74
src/main/java/de/ph87/tools/email/Email.java
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package de.ph87.tools.email;
|
||||||
|
|
||||||
|
import de.ph87.tools.user.User;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@Table(name = "`email`")
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Email {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String uuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private ZonedDateTime queued = ZonedDateTime.now();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String receiver;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private ZonedDateTime tried;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
private int tries = 0;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private ZonedDateTime sent;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status = "";
|
||||||
|
|
||||||
|
public Email(@NonNull final User user, @NonNull final String subject, @NonNull final String content) {
|
||||||
|
this.user = user;
|
||||||
|
this.receiver = user.getEmail();
|
||||||
|
this.subject = subject;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTry() {
|
||||||
|
tried = ZonedDateTime.now();
|
||||||
|
tries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
36
src/main/java/de/ph87/tools/email/EmailConfig.java
Normal file
36
src/main/java/de/ph87/tools/email/EmailConfig.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package de.ph87.tools.email;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "de.ph87.tools.email")
|
||||||
|
public class EmailConfig {
|
||||||
|
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
private int port = 465;
|
||||||
|
|
||||||
|
private boolean ssl = true;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
private String from;
|
||||||
|
|
||||||
|
private String hrefBase = "http://localhost:4200";
|
||||||
|
|
||||||
|
public Properties getProperties() {
|
||||||
|
final Properties properties = new Properties();
|
||||||
|
properties.put("mail.smtp.host", host);
|
||||||
|
properties.put("mail.smtp.port", port);
|
||||||
|
properties.put("mail.smtp.ssl.enable", ssl);
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
src/main/java/de/ph87/tools/email/EmailDto.java
Normal file
52
src/main/java/de/ph87/tools/email/EmailDto.java
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package de.ph87.tools.email;
|
||||||
|
|
||||||
|
import de.ph87.tools.user.User;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class EmailDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String uuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final ZonedDateTime queued;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final User user;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String receiver;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String subject;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
private final String content;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final ZonedDateTime sent;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
private final String status;
|
||||||
|
|
||||||
|
public EmailDto(@NonNull final Email email) {
|
||||||
|
this.uuid = email.getUuid();
|
||||||
|
this.queued = email.getQueued();
|
||||||
|
this.user = email.getUser();
|
||||||
|
this.receiver = "%s <%s>".formatted(user.getName(), user.getEmail());
|
||||||
|
this.subject = email.getSubject();
|
||||||
|
this.content = email.getContent();
|
||||||
|
this.sent = email.getSent();
|
||||||
|
this.status = email.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
src/main/java/de/ph87/tools/email/EmailRepository.java
Normal file
18
src/main/java/de/ph87/tools/email/EmailRepository.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.tools.email;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public interface EmailRepository extends ListCrudRepository<Email, String>, PagingAndSortingRepository<Email, String> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Query("select e from Email e where e.sent is null and (e.tried is null or e.tried <= :earliest) and e.tries < 5")
|
||||||
|
Page<Email> findNextToSend(@NonNull final ZonedDateTime earliest, @NonNull Pageable pageable);
|
||||||
|
|
||||||
|
}
|
||||||
103
src/main/java/de/ph87/tools/email/EmailSender.java
Normal file
103
src/main/java/de/ph87/tools/email/EmailSender.java
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package de.ph87.tools.email;
|
||||||
|
|
||||||
|
import de.ph87.tools.email.events.EmailQueuedEvent;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import jakarta.mail.*;
|
||||||
|
import jakarta.mail.internet.InternetAddress;
|
||||||
|
import jakarta.mail.internet.MimeBodyPart;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import jakarta.mail.internet.MimeMultipart;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EmailSender {
|
||||||
|
|
||||||
|
private final EmailService emailService;
|
||||||
|
|
||||||
|
private final EmailConfig emailConfig;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Thread thread = new Thread(this::run, getClass().getSimpleName());
|
||||||
|
|
||||||
|
private boolean stop = false;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void postConstruct() {
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void preDestroy() throws InterruptedException {
|
||||||
|
synchronized (thread) {
|
||||||
|
stop = true;
|
||||||
|
wakeUp();
|
||||||
|
}
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() {
|
||||||
|
while (!stop) {
|
||||||
|
sendAllUnsent();
|
||||||
|
sleep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAllUnsent() {
|
||||||
|
while (true) {
|
||||||
|
final Page<EmailDto> page = emailService.findNextToSend(10);
|
||||||
|
for (final EmailDto emailDto : page.getContent()) {
|
||||||
|
send(emailDto);
|
||||||
|
}
|
||||||
|
if (page.getTotalElements() - page.getNumberOfElements() <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleep() {
|
||||||
|
try {
|
||||||
|
synchronized (thread) {
|
||||||
|
thread.wait();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send(@NonNull EmailDto emailDto) {
|
||||||
|
try {
|
||||||
|
final Session session = Session.getInstance(emailConfig.getProperties(), null);
|
||||||
|
final Message message = new MimeMessage(session);
|
||||||
|
message.setFrom(new InternetAddress(emailConfig.getFrom()));
|
||||||
|
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(emailDto.getReceiver()));
|
||||||
|
message.setSubject(emailDto.getSubject());
|
||||||
|
final MimeBodyPart mimeBodyPart = new MimeBodyPart();
|
||||||
|
mimeBodyPart.setContent(emailDto.getContent(), "text/html; charset=utf-8");
|
||||||
|
final Multipart multipart = new MimeMultipart();
|
||||||
|
multipart.addBodyPart(mimeBodyPart);
|
||||||
|
message.setContent(multipart);
|
||||||
|
Transport.send(message, emailConfig.getUsername(), emailConfig.getPassword());
|
||||||
|
emailDto = emailService.markSuccess(emailDto.getUuid());
|
||||||
|
log.info("Email sent: email={}", emailDto);
|
||||||
|
} catch (MessagingException e) {
|
||||||
|
emailDto = emailService.markFailure(emailDto.getUuid(), e.toString());
|
||||||
|
log.error("Failed to send email: email={}", emailDto, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(EmailQueuedEvent.class)
|
||||||
|
public void wakeUp() {
|
||||||
|
synchronized (thread) {
|
||||||
|
thread.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
83
src/main/java/de/ph87/tools/email/EmailService.java
Normal file
83
src/main/java/de/ph87/tools/email/EmailService.java
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package de.ph87.tools.email;
|
||||||
|
|
||||||
|
import de.ph87.tools.email.events.EmailQueuedEvent;
|
||||||
|
import de.ph87.tools.user.User;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EmailService {
|
||||||
|
|
||||||
|
private static final String EMAIL_CONTENT = "<html><body><h1>E-Mail Adresse bestätigen</h1><p>Hallo %USERNAME%,<br>bitte klicke auf folgenden Link um deine E-Mail Adresse zu bestätigen: <a href='%HREF_BASE%/emailConfirmation/%EMAIL_CONFIRMATION%'>BESTÄTIGEN</a></p><p>Alternativ kannst Du auch folgenden Code auf deiner Profilseite eingeben: <span style='font-family: monospace; background-color: white; color: blue'>%EMAIL_CONFIRMATION%</span></p></body></html>";
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
private final EmailRepository emailRepository;
|
||||||
|
|
||||||
|
private final EmailConfig emailConfig;
|
||||||
|
|
||||||
|
public void queue(@NonNull final User user, @NonNull final String subject, @NonNull final String content) {
|
||||||
|
final Email email = emailRepository.save(new Email(user, subject, content));
|
||||||
|
log.info("Email queued: {}", email);
|
||||||
|
applicationEventPublisher.publishEvent(new EmailQueuedEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Page<EmailDto> findNextToSend(final int maxCount) {
|
||||||
|
return emailRepository.findNextToSend(ZonedDateTime.now().minusMinutes(1), PageRequest.of(0, maxCount)).map(EmailDto::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public EmailDto markSuccess(@NonNull final String uuid) {
|
||||||
|
return modify(uuid, email -> {
|
||||||
|
email.addTry();
|
||||||
|
email.setSent(email.getTried());
|
||||||
|
email.setStatus("OK");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public EmailDto markFailure(@NonNull final String uuid, @NonNull final String error) {
|
||||||
|
return modify(uuid, email -> {
|
||||||
|
if (email.getSent() != null) {
|
||||||
|
log.error("Cannot mark email failure, because email is already successfully sent: email={}", email);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
email.addTry();
|
||||||
|
email.setStatus(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private EmailDto modify(@NonNull final String uuid, @NonNull final Consumer<Email> modifier) {
|
||||||
|
final Email email = emailRepository.findById(uuid).orElseThrow();
|
||||||
|
modifier.accept(email);
|
||||||
|
return new EmailDto(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queueEmailConfirmationEmail(@NonNull final User user) {
|
||||||
|
final String content = createEmailContent(user);
|
||||||
|
queue(user, "Patrix Tools: Email-Adresse bestätigen", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String createEmailContent(@NonNull final User user) {
|
||||||
|
return EMAIL_CONTENT
|
||||||
|
.replaceAll("%USERNAME%", user.getName())
|
||||||
|
.replaceAll("%HREF_BASE%", emailConfig.getHrefBase())
|
||||||
|
.replaceAll("%EMAIL_CONFIRMATION%", user.getEmailConfirmation());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package de.ph87.tools.email.events;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class EmailQueuedEvent {
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package de.ph87.tools.tools.numbers;
|
package de.ph87.tools.tools.numbers;
|
||||||
|
|
||||||
|
import de.ph87.tools.group.GroupMapper;
|
||||||
import de.ph87.tools.group.access.GroupAccess;
|
import de.ph87.tools.group.access.GroupAccess;
|
||||||
import de.ph87.tools.group.access.GroupAccessService;
|
import de.ph87.tools.group.access.GroupAccessService;
|
||||||
import de.ph87.tools.group.GroupMapper;
|
|
||||||
import de.ph87.tools.group.dto.GroupDto;
|
import de.ph87.tools.group.dto.GroupDto;
|
||||||
import de.ph87.tools.group.uuid.GroupUuid;
|
import de.ph87.tools.group.uuid.GroupUuid;
|
||||||
import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
|
import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
|
||||||
|
|||||||
@ -72,12 +72,20 @@ public class User extends UserPublicAbstract {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String password = "";
|
private String password = "";
|
||||||
|
|
||||||
@Setter
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String email = "";
|
private String email = "";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String emailConfirmation = "";
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean emailConfirmed = false;
|
||||||
|
|
||||||
public User(@NonNull final String name) {
|
public User(@NonNull final String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
@ -90,4 +98,21 @@ public class User extends UserPublicAbstract {
|
|||||||
this.password = passwordEncoder.encode(password);
|
this.password = passwordEncoder.encode(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean verifyPassword(@NonNull final PasswordEncoder passwordEncoder, @NonNull final String password) {
|
||||||
|
return passwordEncoder.matches(password, this.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(@NonNull final String email) {
|
||||||
|
if (this.email.equals(email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.email = email;
|
||||||
|
this.emailConfirmed = false;
|
||||||
|
if (this.email.isEmpty()) {
|
||||||
|
this.emailConfirmation = "";
|
||||||
|
} else {
|
||||||
|
this.emailConfirmation = UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,8 @@ public class UserController {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@GetMapping("whoAmI")
|
@GetMapping("whoAmI")
|
||||||
public UserPrivateDto whoAmI(@Nullable final UserPrivateUuid userUuid, @NonNull final HttpServletResponse response) {
|
public UserPrivateDto whoAmI(@Nullable final UserPrivateUuid userPrivateUuid, @NonNull final HttpServletResponse response) {
|
||||||
return userService.whoAmI(userUuid, response);
|
return userService.whoAmI(userPrivateUuid, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -32,32 +32,42 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@GetMapping("login")
|
@PostMapping("login")
|
||||||
public UserPrivateDto login(@NonNull @RequestBody final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) {
|
public UserPrivateDto login(@NonNull @RequestBody final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) {
|
||||||
return userService.login(loginRequest, response);
|
return userService.login(loginRequest, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("logout")
|
||||||
|
public void logout(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final HttpServletResponse response) {
|
||||||
|
userService.logout(userPrivateUuid, response);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@PostMapping("changeName")
|
@PostMapping("changeName")
|
||||||
public UserPrivateDto changeName(@NonNull final UserPrivateUuid userUuid, @NonNull @RequestBody final String name) {
|
public UserPrivateDto changeName(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull @RequestBody final String name) {
|
||||||
return userService.changeName(userUuid, name);
|
return userService.changeName(userPrivateUuid, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@PostMapping("changePassword")
|
@PostMapping("changePassword")
|
||||||
public UserPrivateDto changePassword(@NonNull final UserPrivateUuid userUuid, @NonNull @RequestBody final String password) {
|
public UserPrivateDto changePassword(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull @RequestBody final String password) {
|
||||||
return userService.changePassword(userUuid, password);
|
return userService.changePassword(userPrivateUuid, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@PostMapping("changeEmail")
|
@PostMapping("changeEmail")
|
||||||
public UserPrivateDto changeEmail(@NonNull final UserPrivateUuid userUuid, @NonNull @RequestBody final String email) {
|
public UserPrivateDto changeEmail(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull @RequestBody final String email) {
|
||||||
return userService.changeEmail(userUuid, email);
|
return userService.changeEmail(userPrivateUuid, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("confirmEmail")
|
||||||
|
public boolean confirmEmail(@NonNull @RequestBody final String emailConfirmation) {
|
||||||
|
return userService.confirmEmail(emailConfirmation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("delete")
|
@GetMapping("delete")
|
||||||
public void delete(@NonNull final UserPrivateUuid userUuid, @NonNull final HttpServletResponse response) {
|
public void delete(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final HttpServletResponse response) {
|
||||||
userService.delete(userUuid, response);
|
userService.delete(userPrivateUuid, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,8 @@ public class UserPrivateDto extends UserPublicAbstract {
|
|||||||
|
|
||||||
private final String email;
|
private final String email;
|
||||||
|
|
||||||
|
private final boolean emailConfirmed;
|
||||||
|
|
||||||
private final boolean admin;
|
private final boolean admin;
|
||||||
|
|
||||||
public UserPrivateDto(@NonNull final User user) {
|
public UserPrivateDto(@NonNull final User user) {
|
||||||
@ -48,6 +50,7 @@ public class UserPrivateDto extends UserPublicAbstract {
|
|||||||
this.created = user.getCreated();
|
this.created = user.getCreated();
|
||||||
this.password = !user.getPassword().isEmpty();
|
this.password = !user.getPassword().isEmpty();
|
||||||
this.email = obfuscateEmail(user.getEmail());
|
this.email = obfuscateEmail(user.getEmail());
|
||||||
|
this.emailConfirmed = user.isEmailConfirmed();
|
||||||
this.admin = user.isAdmin();
|
this.admin = user.isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,4 +17,7 @@ public interface UserRepository extends ListCrudRepository<User, String> {
|
|||||||
@NonNull
|
@NonNull
|
||||||
Optional<User> findByPrivateUuid(@NonNull String privateUuid);
|
Optional<User> findByPrivateUuid(@NonNull String privateUuid);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Optional<User> findByEmailConfirmation(@NonNull String emailConfirmation);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.ph87.tools.user;
|
package de.ph87.tools.user;
|
||||||
|
|
||||||
import de.ph87.tools.common.uuid.AbstractUuid;
|
import de.ph87.tools.common.uuid.AbstractUuid;
|
||||||
|
import de.ph87.tools.email.EmailService;
|
||||||
import de.ph87.tools.group.GroupMapper;
|
import de.ph87.tools.group.GroupMapper;
|
||||||
import de.ph87.tools.group.GroupRepository;
|
import de.ph87.tools.group.GroupRepository;
|
||||||
import de.ph87.tools.user.push.UserPushService;
|
import de.ph87.tools.user.push.UserPushService;
|
||||||
@ -57,15 +58,16 @@ public class UserService {
|
|||||||
|
|
||||||
private final UserPushService userPushService;
|
private final UserPushService userPushService;
|
||||||
|
|
||||||
@NonNull
|
private final EmailService emailService;
|
||||||
public UserPrivateDto login(@NonNull final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) {
|
|
||||||
final User user = userRepository.findByName(loginRequest.name).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
@Nullable
|
||||||
if (passwordEncoder.matches(loginRequest.password, user.getPassword())) {
|
public UserPrivateDto whoAmI(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
if (privateUuid == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
user.touch();
|
final User user = userRepository.findByPrivateUuid(privateUuid.uuid).orElse(null);
|
||||||
writeUserUuidCookie(response, user);
|
writeUserUuidCookie(response, user);
|
||||||
return new UserPrivateDto(user);
|
return UserPrivateDto.orNull(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -81,14 +83,33 @@ public class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NonNull
|
||||||
public UserPrivateDto whoAmI(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
|
public UserPrivateDto login(@NonNull final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) {
|
||||||
if (privateUuid == null) {
|
final Optional<User> userOptional = userRepository.findByName(loginRequest.username);
|
||||||
return null;
|
if (userOptional.isEmpty()) {
|
||||||
|
log.warn("Login failed: Unknown user: username={}", loginRequest.username);
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
final User user = userRepository.findByPrivateUuid(privateUuid.uuid).orElse(null);
|
final User user = userOptional.get();
|
||||||
|
if (user.getPassword().isEmpty()) {
|
||||||
|
log.warn("Login failed: No password set: user={}", user);
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (!user.verifyPassword(passwordEncoder, loginRequest.password)) {
|
||||||
|
log.warn("Login failed: Wrong password: user={}", user);
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
user.touch();
|
||||||
writeUserUuidCookie(response, user);
|
writeUserUuidCookie(response, user);
|
||||||
return UserPrivateDto.orNull(user);
|
log.info("Login successful: user={}", user);
|
||||||
|
return new UserPrivateDto(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void logout(@NonNull final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
|
||||||
|
modify(privateUuid, user -> {
|
||||||
|
writeUserUuidCookie(response, null);
|
||||||
|
log.info("Logout successful: user={}", user);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -139,11 +160,29 @@ public class UserService {
|
|||||||
log.warn("Cannot change User email: not valid, user={}", user);
|
log.warn("Cannot change User email: not valid, user={}", user);
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
if (user.getEmail().equals(email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
user.setEmail(email);
|
user.setEmail(email);
|
||||||
log.info("User email changed: user={}", user);
|
log.info("User email changed: user={}", user);
|
||||||
|
emailService.queueEmailConfirmationEmail(user);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean confirmEmail(@NonNull final String emailConfirmation) {
|
||||||
|
final User user = userRepository.findByEmailConfirmation(emailConfirmation).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
log.warn("Failed to confirm email: emailConfirmation={}", emailConfirmation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!user.isEmailConfirmed()) {
|
||||||
|
user.setEmailConfirmed(true);
|
||||||
|
log.info("User email confirmed: user={}", user);
|
||||||
|
push(user);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void delete(@NonNull final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
|
public void delete(@NonNull final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
|
||||||
final User user = userRepository.findByPrivateUuid(privateUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
final User user = userRepository.findByPrivateUuid(privateUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
|
||||||
deleteUnchecked(response, user);
|
deleteUnchecked(response, user);
|
||||||
@ -174,12 +213,16 @@ public class UserService {
|
|||||||
private UserPrivateDto modify(@NonNull final UserPrivateUuid privateUuid, @NonNull Consumer<User> modifier) {
|
private UserPrivateDto modify(@NonNull final UserPrivateUuid privateUuid, @NonNull Consumer<User> modifier) {
|
||||||
final User user = userAccessService.access(privateUuid);
|
final User user = userAccessService.access(privateUuid);
|
||||||
modifier.accept(user);
|
modifier.accept(user);
|
||||||
|
return push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull UserPrivateDto push(@NonNull final User user) {
|
||||||
final UserPrivateDto privateDto = new UserPrivateDto(user);
|
final UserPrivateDto privateDto = new UserPrivateDto(user);
|
||||||
userPushService.push(user, privateDto);
|
userPushService.push(user, privateDto);
|
||||||
return privateDto;
|
return privateDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteUnchecked(final HttpServletResponse response, final User user) {
|
private void deleteUnchecked(@NonNull final HttpServletResponse response, @NonNull final User user) {
|
||||||
userRepository.delete(user);
|
userRepository.delete(user);
|
||||||
log.info("User DELETED: {}", user);
|
log.info("User DELETED: {}", user);
|
||||||
writeUserUuidCookie(response, null);
|
writeUserUuidCookie(response, null);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import lombok.ToString;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserLoginRequest {
|
public class UserLoginRequest {
|
||||||
|
|
||||||
public final String name;
|
public final String username;
|
||||||
|
|
||||||
public final String password;
|
public final String password;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user