login, logout

This commit is contained in:
Patrick Haßel 2024-11-07 14:23:07 +01:00
parent ac82b8a0ac
commit 530dcd2f04
16 changed files with 211 additions and 64 deletions

View File

@ -1,12 +1,6 @@
import {validateBoolean, validateDate, validateString} from "../common/validators"; import {validateBoolean, validateDate, validateString} from "../common/validators";
import {UserPublic} from "./UserPublic"; import {UserPublic} from "./UserPublic";
export enum EmailStatus {
NOT_SET = 'NOT_SET',
CONFIRMATION_NEEDED = 'CONFIRMATION_NEEDED',
CONFIRMED = 'CONFIRMED',
}
export class UserPrivate extends UserPublic { export class UserPrivate extends UserPublic {
constructor( constructor(

View File

@ -0,0 +1,10 @@
export class UserLoginRequest {
constructor(
readonly username: string,
readonly password: string,
) {
// -
}
}

View File

@ -12,6 +12,7 @@ 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 {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_'];
@ -60,7 +61,22 @@ export class UserService {
} }
private fetchUser() { private 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) {
@ -118,4 +134,13 @@ export class UserService {
) )
.subscribe(next); .subscribe(next);
} }
gotoLogin() {
this.router.navigate(['Login']);
}
gotoProfile() {
this.router.navigate(['Profile']);
}
} }

View File

@ -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)"/>

View File

@ -8,6 +8,7 @@ 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 {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},
@ -22,6 +23,7 @@ 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}, {path: 'emailConfirmation/:emailConfirmation', component: EmailConfirmationComponent},

View File

@ -4,7 +4,6 @@ import {ActivatedRoute, Router} from "@angular/router";
import {GroupService} from "../../api/group/group.service"; import {GroupService} from "../../api/group/group.service";
import {UserService} from "../../api/User/user.service"; import {UserService} from "../../api/User/user.service";
import {NgIf} from "@angular/common"; import {NgIf} from "@angular/common";
import {EmailStatus} from "../../api/User/UserPrivate";
@Component({ @Component({
selector: 'app-email-confirmation', selector: 'app-email-confirmation',
@ -46,5 +45,4 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
this.subs.length = 0; this.subs.length = 0;
} }
protected readonly EmailStatus = EmailStatus;
} }

View 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>

View File

@ -0,0 +1,7 @@
input {
width: 100%;
}
.button {
width: 100%;
}

View 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;
}

View File

@ -47,6 +47,21 @@
</div> </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>

View File

@ -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 {EmailStatus, UserPrivate} from "../../api/User/UserPrivate";
const USER_NAME_MIN_LENGTH = 2; const USER_NAME_MIN_LENGTH = 2;
@ -26,22 +23,18 @@ 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 email: string = ""; protected email: string = "";
protected groups: Group[] = [];
@ViewChild('p1') @ViewChild('p1')
p1!: ElementRef; p1!: ElementRef;
@ -52,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);
} }
@ -117,5 +94,16 @@ export class ProfileComponent implements OnInit, OnDestroy {
}); });
} }
protected readonly EmailStatus = EmailStatus; 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()
}
} }

View File

@ -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;

View File

@ -98,6 +98,10 @@ 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) { public void setEmail(@NonNull final String email) {
if (this.email.equals(email)) { if (this.email.equals(email)) {
return; return;

View File

@ -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,27 +32,32 @@ 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") @PostMapping("confirmEmail")
@ -61,8 +66,8 @@ public class UserController {
} }
@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);
} }
} }

View File

@ -60,15 +60,14 @@ public class UserService {
private final EmailService emailService; private final EmailService emailService;
@NonNull @Nullable
public UserPrivateDto login(@NonNull final UserLoginRequest loginRequest, @NonNull final HttpServletResponse response) { public UserPrivateDto whoAmI(@Nullable final UserPrivateUuid privateUuid, @NonNull final HttpServletResponse response) {
final User user = userRepository.findByName(loginRequest.name).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST)); if (privateUuid == null) {
if (passwordEncoder.matches(loginRequest.password, user.getPassword())) { return null;
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
} }
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
@ -84,19 +83,38 @@ 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
public UserPrivateDto changeName(@NonNull final UserPrivateUuid privateUuid, @NonNull final String name) { public UserPrivateDto changeName(@NonNull final UserPrivateUuid privateUuid, @NonNull final String name) {
return modifyToDto(privateUuid, user -> { return modify(privateUuid, user -> {
if (user.getName().equals(name)) { if (user.getName().equals(name)) {
return; return;
} }
@ -121,7 +139,7 @@ public class UserService {
@NonNull @NonNull
public UserPrivateDto changePassword(@NonNull final UserPrivateUuid privateUuid, @NonNull final String password) { public UserPrivateDto changePassword(@NonNull final UserPrivateUuid privateUuid, @NonNull final String password) {
return modifyToDto(privateUuid, user -> { return modify(privateUuid, user -> {
if (password.length() < PASSWORD_MIN_LENGTH) { if (password.length() < PASSWORD_MIN_LENGTH) {
log.warn("Cannot change User password: too short: length={}/{}, user={}", password.length(), PASSWORD_MIN_LENGTH, user); log.warn("Cannot change User password: too short: length={}/{}, user={}", password.length(), PASSWORD_MIN_LENGTH, user);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST); throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
@ -137,7 +155,7 @@ public class UserService {
@NonNull @NonNull
public UserPrivateDto changeEmail(@NonNull final UserPrivateUuid privateUuid, @NonNull final String email) { public UserPrivateDto changeEmail(@NonNull final UserPrivateUuid privateUuid, @NonNull final String email) {
return modifyToDto(privateUuid, user -> { return modify(privateUuid, user -> {
if (!isEmailValid(email)) { if (!isEmailValid(email)) {
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);
@ -192,7 +210,7 @@ public class UserService {
} }
@NonNull @NonNull
private UserPrivateDto modifyToDto(@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); return push(user);

View File

@ -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;