From 23a7216de96aaf9ecafc1bed58cf378652c93a68 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sun, 8 Sep 2024 17:16:07 +0200 Subject: [PATCH] pw reset --- idp/src/auth/auth.controller.ts | 6 ++ idp/src/auth/auth.module.ts | 2 + idp/src/core/database/database.module.ts | 3 + idp/src/core/secure/secure.module.ts | 2 + idp/src/model/dto/index.ts | 2 + idp/src/model/dto/request-reset-pw.dto.ts | 3 + idp/src/model/dto/resetPw.dto.ts | 4 + idp/src/model/reset-pw.entity.ts | 33 ++++++++ idp/src/users/users.service.ts | 48 +++++++++++- idp_client/src/app/app.routes.ts | 2 + .../src/app/auth/login/login.component.html | 7 +- .../src/app/auth/login/login.component.ts | 4 + .../app/auth/reset-pw/reset-pw.component.html | 51 +++++++++++++ .../auth/reset-pw/reset-pw.component.spec.ts | 23 ++++++ .../app/auth/reset-pw/reset-pw.component.ts | 76 +++++++++++++++++++ .../src/app/core/guards/session-key.guard.ts | 2 + 16 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 idp/src/model/dto/request-reset-pw.dto.ts create mode 100644 idp/src/model/dto/resetPw.dto.ts create mode 100644 idp/src/model/reset-pw.entity.ts create mode 100644 idp_client/src/app/auth/reset-pw/reset-pw.component.html create mode 100644 idp_client/src/app/auth/reset-pw/reset-pw.component.spec.ts create mode 100644 idp_client/src/app/auth/reset-pw/reset-pw.component.ts diff --git a/idp/src/auth/auth.controller.ts b/idp/src/auth/auth.controller.ts index 8f58252..ae1f80e 100644 --- a/idp/src/auth/auth.controller.ts +++ b/idp/src/auth/auth.controller.ts @@ -5,6 +5,7 @@ import { FormDataRequest } from 'nestjs-form-data'; import { Client } from 'src/model/client.entity'; import { CustomLogger } from 'src/core/custom.logger'; import { CreateUserDto } from 'src/model/dto/create-user.dto'; +import { RequestResetPwDto, ResetPWDto } from 'src/model/dto'; @Controller('auth') export class AuthController { @@ -20,6 +21,11 @@ export class AuthController { return user; } + @Post('reset') + async resetPw(@Body() b: RequestResetPwDto | ResetPWDto) { + return this.usersService.resetPw(b); + } + @Post('login') async login( @Body('username') username: string, diff --git a/idp/src/auth/auth.module.ts b/idp/src/auth/auth.module.ts index bce61ab..8ec13ae 100644 --- a/idp/src/auth/auth.module.ts +++ b/idp/src/auth/auth.module.ts @@ -17,6 +17,7 @@ import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { Role, RoleRepository } from 'src/model/role.entity'; import { DatabaseModule } from 'src/core/database/database.module'; +import { MailModule } from 'src/application/mail/mail.module'; @Module({ providers: [ @@ -49,6 +50,7 @@ import { DatabaseModule } from 'src/core/database/database.module'; ]), LoggerModule, DatabaseModule, + MailModule, ], }) export class AuthModule {} diff --git a/idp/src/core/database/database.module.ts b/idp/src/core/database/database.module.ts index c514ac1..e0b2155 100644 --- a/idp/src/core/database/database.module.ts +++ b/idp/src/core/database/database.module.ts @@ -7,6 +7,7 @@ import { import { Client, ClientRepository } from 'src/model/client.entity'; import { Log, LogRepository } from 'src/model/log.entity'; import { RedirectRepository, RedirectUri } from 'src/model/redirect-uri.entity'; +import { ResetPwCode, ResetPwCodeRepository } from 'src/model/reset-pw.entity'; import { SessionKey, SessionKeyRepository } from 'src/model/session-key.entity'; import { User, UserRepository } from 'src/model/user.entity'; @@ -17,6 +18,7 @@ const ENTITIES = [ AuthorizationCode, SessionKey, Log, + ResetPwCode, ]; const REPOSITORIES = [ UserRepository, @@ -25,6 +27,7 @@ const REPOSITORIES = [ SessionKeyRepository, RedirectRepository, LogRepository, + ResetPwCodeRepository, ]; @Module({ diff --git a/idp/src/core/secure/secure.module.ts b/idp/src/core/secure/secure.module.ts index 2b8afca..bf190f4 100644 --- a/idp/src/core/secure/secure.module.ts +++ b/idp/src/core/secure/secure.module.ts @@ -8,6 +8,7 @@ import { LoggerModule } from '../logger.module'; import { AuthGuard } from './guards/auth.guard'; import { DatabaseModule } from '../database/database.module'; import { RolesGuard } from './guards/roles.guard'; +import { MailModule } from 'src/application/mail/mail.module'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { RolesGuard } from './guards/roles.guard'; NestjsFormDataModule, DatabaseModule, LoggerModule, + MailModule, ], providers: [UsersService, ClientService, AuthGuard, RolesGuard], exports: [JwtModule, UsersService, AuthGuard, RolesGuard], diff --git a/idp/src/model/dto/index.ts b/idp/src/model/dto/index.ts index baa5dbd..6aab46f 100644 --- a/idp/src/model/dto/index.ts +++ b/idp/src/model/dto/index.ts @@ -1,2 +1,4 @@ export * from './create-user.dto'; export * from './login-user.dto'; +export * from './resetPw.dto'; +export * from './request-reset-pw.dto'; diff --git a/idp/src/model/dto/request-reset-pw.dto.ts b/idp/src/model/dto/request-reset-pw.dto.ts new file mode 100644 index 0000000..5bce873 --- /dev/null +++ b/idp/src/model/dto/request-reset-pw.dto.ts @@ -0,0 +1,3 @@ +export interface RequestResetPwDto { + username: string; +} diff --git a/idp/src/model/dto/resetPw.dto.ts b/idp/src/model/dto/resetPw.dto.ts new file mode 100644 index 0000000..2ff88fb --- /dev/null +++ b/idp/src/model/dto/resetPw.dto.ts @@ -0,0 +1,4 @@ +export interface ResetPWDto { + password: string; + code: string; +} diff --git a/idp/src/model/reset-pw.entity.ts b/idp/src/model/reset-pw.entity.ts new file mode 100644 index 0000000..7a8f05f --- /dev/null +++ b/idp/src/model/reset-pw.entity.ts @@ -0,0 +1,33 @@ +import { + Entity, + PrimaryGeneratedColumn, + ManyToOne, + DataSource, + Repository, + CreateDateColumn, +} from 'typeorm'; +import { User } from './user.entity'; +import { Injectable } from '@nestjs/common'; + +@Entity() +export class ResetPwCode { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.sessionKeys, { eager: true }) + user: User; + + @CreateDateColumn() + createdAt: Date; +} + +@Injectable() +export class ResetPwCodeRepository extends Repository { + constructor(dataSource: DataSource) { + super(ResetPwCode, dataSource.createEntityManager()); + } + + findById(id: string): Promise { + return this.findOneBy({ id }); + } +} diff --git a/idp/src/users/users.service.ts b/idp/src/users/users.service.ts index eeba757..6684ceb 100644 --- a/idp/src/users/users.service.ts +++ b/idp/src/users/users.service.ts @@ -1,4 +1,4 @@ -import { HttpException, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { User, UserRepository } from 'src/model/user.entity'; import { v4 as uuidv4 } from 'uuid'; import * as bcrypt from 'bcrypt'; @@ -12,7 +12,9 @@ import { CustomLogger } from 'src/core/custom.logger'; import { CreateUserDto } from 'src/model/dto/create-user.dto'; import { SessionKeyRepository } from 'src/model/session-key.entity'; import { Client } from 'src/model/client.entity'; -import { LoginUserDto } from 'src/model/dto'; +import { LoginUserDto, RequestResetPwDto, ResetPWDto } from 'src/model/dto'; +import { ResetPwCodeRepository } from 'src/model/reset-pw.entity'; +import { MailService } from 'src/application/mail/mail.service'; @Injectable() export class UsersService { @@ -23,6 +25,8 @@ export class UsersService { private tokenRepo: AuthorizationCodeRepository, private sessionRepo: SessionKeyRepository, private logger: CustomLogger, + private resetPwRepo: ResetPwCodeRepository, + private mailService: MailService, ) {} async createUser(userDto: CreateUserDto): Promise { const hashedPassword = await bcrypt.hash(userDto.password, 10); @@ -101,7 +105,7 @@ export class UsersService { throw new HttpException('Invalid client', 401); } - const session = await this.sessionRepo.findOneByOrFail({ id: sessionKey }); + const session = await this.sessionRepo.findOneBy({ id: sessionKey }); if (!session) { throw new HttpException('Invalid session key', 401); } @@ -265,4 +269,42 @@ export class UsersService { user.session_key = session.id; return user; } + + async resetPw(dto: RequestResetPwDto | ResetPWDto) { + if (dto['username'] != null) { + // Send Mail + dto = dto as RequestResetPwDto; + const user = await this.userRepo.findOneBy({ username: dto.username }); + + const code = await this.resetPwRepo.save( + this.resetPwRepo.create({ user }), + ); + + await this.mailService.sendResetMail({ + code: code.id, + name: user.firstName, + to: user.username, + url: 'pw-reset', + }); + return { success: true }; + } else if (dto['password'] && dto['code']) { + // neues PW setzen + dto = dto as ResetPWDto; + + const savedCode = await this.resetPwRepo.findOne({ + where: { id: dto.code }, + relations: ['user'], + }); + + if (savedCode && savedCode.user) { + const hashedPassword = await bcrypt.hash(dto.password, 10); + savedCode.user.password = hashedPassword; + await this.userRepo.save(savedCode.user); + await this.resetPwRepo.remove(savedCode); + await this.sessionRepo.delete({ user: { id: savedCode.user.id }}); + return { success: true }; + } + } + throw new HttpException('unprocessible entity', HttpStatus.UNAUTHORIZED); + } } diff --git a/idp_client/src/app/app.routes.ts b/idp_client/src/app/app.routes.ts index 5fe29b2..3160a6b 100644 --- a/idp_client/src/app/app.routes.ts +++ b/idp_client/src/app/app.routes.ts @@ -3,10 +3,12 @@ import { LoginComponent } from './auth/login/login.component'; import { RegisterComponent } from './auth/register/register.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { SessionKeyGuard } from './core/guards/session-key.guard'; +import { ResetPwComponent } from './auth/reset-pw/reset-pw.component'; export const routes: Routes = [ { path: 'login', component: LoginComponent, canActivate: [SessionKeyGuard] }, { path: 'register', component: RegisterComponent }, + { path: 'pw-reset', component: ResetPwComponent }, { path: 'dashboard', component: DashboardComponent, canActivate: [SessionKeyGuard] }, { path: '', component: LoginComponent, canActivate: [SessionKeyGuard] }, ]; diff --git a/idp_client/src/app/auth/login/login.component.html b/idp_client/src/app/auth/login/login.component.html index 0fd9c27..f93a27a 100644 --- a/idp_client/src/app/auth/login/login.component.html +++ b/idp_client/src/app/auth/login/login.component.html @@ -12,10 +12,13 @@ - +
+ + +
diff --git a/idp_client/src/app/auth/login/login.component.ts b/idp_client/src/app/auth/login/login.component.ts index 2eb1194..85fb53d 100644 --- a/idp_client/src/app/auth/login/login.component.ts +++ b/idp_client/src/app/auth/login/login.component.ts @@ -106,4 +106,8 @@ export class LoginComponent { toRegister() { this.router.navigate(['/register'], { queryParams: this.route.snapshot.queryParams }); } + + toResetPw() { + this.router.navigateByUrl('/pw-reset') + } } diff --git a/idp_client/src/app/auth/reset-pw/reset-pw.component.html b/idp_client/src/app/auth/reset-pw/reset-pw.component.html new file mode 100644 index 0000000..2ca1402 --- /dev/null +++ b/idp_client/src/app/auth/reset-pw/reset-pw.component.html @@ -0,0 +1,51 @@ +
+
+
+ + @if (resetCode == null) { + + } @else { + + } +
+
+ + + + +
+
+
\ No newline at end of file diff --git a/idp_client/src/app/auth/reset-pw/reset-pw.component.spec.ts b/idp_client/src/app/auth/reset-pw/reset-pw.component.spec.ts new file mode 100644 index 0000000..000ced1 --- /dev/null +++ b/idp_client/src/app/auth/reset-pw/reset-pw.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResetPwComponent } from './reset-pw.component'; + +describe('ResetPwComponent', () => { + let component: ResetPwComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResetPwComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ResetPwComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/idp_client/src/app/auth/reset-pw/reset-pw.component.ts b/idp_client/src/app/auth/reset-pw/reset-pw.component.ts new file mode 100644 index 0000000..8f5428a --- /dev/null +++ b/idp_client/src/app/auth/reset-pw/reset-pw.component.ts @@ -0,0 +1,76 @@ +import { HttpClient } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HotToastService } from '@ngxpert/hot-toast'; + +@Component({ + selector: 'app-reset-pw', + standalone: true, + imports: [FormsModule, ReactiveFormsModule], + templateUrl: './reset-pw.component.html', + styleUrl: '../auth.scss' +}) +export class ResetPwComponent { + private router = inject(Router); + private http = inject(HttpClient); + private route = inject(ActivatedRoute); + private toast = inject(HotToastService); + + isLoading = false; + + resetCode; + + resetPw = new FormGroup({ + username: new FormControl('', [Validators.required, Validators.email]) + }) + + setNewPwForm = new FormGroup({ + password: new FormControl(null, [Validators.required, Validators.minLength(4), Validators.maxLength(100)]), + repeatPassword: new FormControl(null, [Validators.required, Validators.minLength(4), Validators.maxLength(100)]), + code: new FormControl(null) + }) + + ngOnInit(): void { + this.resetCode = this.route.snapshot.queryParams["resetcode"]; + this.setNewPwForm.patchValue({code: this.resetCode}); + } + + + resetPassword() { + this.http.post('api/auth/reset', this.resetPw.value).subscribe({ + next: res => { + console.log(res); + } + }) + } + + setNewPassword() { + const val = this.setNewPwForm.value; + if (val.password != val.repeatPassword) { + this.toast.error('Die Passwörter stimmen nicht überein'); + return; + } + this.http.post('api/auth/reset', this.setNewPwForm.value) + .pipe( + this.toast.observe({ + loading: 'Setze neues Passwort', + success: 'Passwort gespeichert', + error: 'Passwort konnte nicht gespeichert werden!' + }) + ) + .subscribe({ + next: res => { + console.log(res); + }, + complete: () => { + this.router.navigateByUrl('/login'); + } + }) + } + + toLogin() { + this.router.navigateByUrl("/login"); + } + +} diff --git a/idp_client/src/app/core/guards/session-key.guard.ts b/idp_client/src/app/core/guards/session-key.guard.ts index 7f82f0c..7b626ee 100644 --- a/idp_client/src/app/core/guards/session-key.guard.ts +++ b/idp_client/src/app/core/guards/session-key.guard.ts @@ -57,6 +57,8 @@ export class SessionKeyGuard { }, error: (error) => { console.error(error); + window.localStorage.removeItem("auth_session_key") + this.router.navigateByUrl('/login'); } }); })