From e5d0387219fec7ae709a35e48ad4fbc80c81ad8d Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Wed, 15 May 2024 21:08:41 +0200 Subject: [PATCH 01/26] added register / login view --- frontend/src/app/app.component.html | 1 - frontend/src/app/app.routes.ts | 1 + .../register-root.component.html | 60 +++++++++++++++ .../register-root.component.scss | 77 +++++++++++++++++++ .../register-root/register-root.component.ts | 50 ++++++++++++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/pages/register-root/register-root.component.html create mode 100644 frontend/src/app/pages/register-root/register-root.component.scss create mode 100644 frontend/src/app/pages/register-root/register-root.component.ts diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 5e5bd89..0680b43 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,2 +1 @@ - diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index e05d7c3..3fb06b1 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -2,4 +2,5 @@ import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: ''}, + { path: 'signup', loadComponent: () => import('./pages/register-root/register-root.component').then(m => m.RegisterRootComponent) }, ]; diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html new file mode 100644 index 0000000..403d1bf --- /dev/null +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -0,0 +1,60 @@ +
+
+
+

Hi, Welcome to Ticket App.

+
+
+
+

+ @if (this.isSignupSignal()) { + Registrieren + } + @else if (this.isRegisterSignal()) { + Anmelden + } + @else { + Erste Schritte + } +

+ + @if (this.isDisplayButtons()) { +
+ + +
+ } + + @if (this.isRegisterSignal()) { +
+
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+ } +
+
diff --git a/frontend/src/app/pages/register-root/register-root.component.scss b/frontend/src/app/pages/register-root/register-root.component.scss new file mode 100644 index 0000000..e2dd579 --- /dev/null +++ b/frontend/src/app/pages/register-root/register-root.component.scss @@ -0,0 +1,77 @@ +#background { + display: flex; + height: 100%; +} + +.img-zone { + flex: 65; + background-color: lightsalmon; + display: flex; + + .img-wrapper { + display: flex; + align-items: center; + + h1 { + font-size: 4em; + margin-left: 1em; + } + } +} + +.content-zone { + flex: 35; + background-color: lightcyan; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + .action { + display: flex; + justify-content: center; + gap: 1em; + + button { + min-width: 200px; + } + } + + .register-wrapper { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + h1 { + font-size: 3em; + } + + .label { + padding: 0 0 0.5em; + } + + .e-mail, .password { + .label { + font-size: 1.5em; + } + + input { + min-width: 500px; + } + } + + .password { + padding-top: 3em; + } + + .signup { + padding-top: 3em; + + button { + min-width: 500px; + } + } + } +} diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts new file mode 100644 index 0000000..1bf1218 --- /dev/null +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -0,0 +1,50 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, OnInit, WritableSignal, signal } from '@angular/core'; +import { InputTextModule } from 'primeng/inputtext'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { ButtonModule } from 'primeng/button'; + +type AuthAction = 'register' | 'signup'; + +@Component({ + selector: 'app-register-root', + standalone: true, + imports: [ + CommonModule, + FormsModule, + InputTextModule, + ReactiveFormsModule, + ButtonModule + ], + templateUrl: './register-root.component.html', + styleUrl: './register-root.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegisterRootComponent implements OnInit { + + public form: FormGroup | undefined; + public isRegisterSignal: WritableSignal = signal(false) + public isSignupSignal: WritableSignal = signal(false) + public isDisplayButtons: WritableSignal = signal(true) + + constructor(private readonly formBuilder: FormBuilder) { } + + ngOnInit() { + this.form = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]] + }); + } + + public toggleAction(action: AuthAction) { + const isRegister = action === 'register'; + this.isRegisterSignal.set(isRegister); + this.isSignupSignal.set(!isRegister); + this.isDisplayButtons.set(false); + } + + public onSubmit() { + console.log('signup'); + } + +} -- 2.40.1 From 2c3db04acde8f7083c055f5265e62854529fc0ac Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Wed, 15 May 2024 21:09:22 +0200 Subject: [PATCH 02/26] run prettier --- frontend/src/app/app.config.ts | 5 +- frontend/src/app/app.routes.ts | 10 +++- .../register-root.component.html | 24 +++++---- .../register-root.component.scss | 3 +- .../register-root/register-root.component.ts | 54 +++++++++++-------- frontend/src/environments/environment.prod.ts | 4 +- frontend/src/environments/environment.ts | 4 +- frontend/src/styles.scss | 6 +++ 8 files changed, 70 insertions(+), 40 deletions(-) diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 0634521..74b2c21 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -5,5 +5,8 @@ import { provideAnimations } from '@angular/platform-browser/animations'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [provideRouter(routes, withComponentInputBinding()), provideAnimations()], + providers: [ + provideRouter(routes, withComponentInputBinding()), + provideAnimations(), + ], }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 3fb06b1..1a9c7a0 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,6 +1,12 @@ import { Routes } from '@angular/router'; export const routes: Routes = [ - { path: '', pathMatch: 'full', redirectTo: ''}, - { path: 'signup', loadComponent: () => import('./pages/register-root/register-root.component').then(m => m.RegisterRootComponent) }, + { path: '', pathMatch: 'full', redirectTo: '' }, + { + path: 'signup', + loadComponent: () => + import('./pages/register-root/register-root.component').then( + (m) => m.RegisterRootComponent + ), + }, ]; diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 403d1bf..0fcfeb1 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -8,19 +8,25 @@

@if (this.isSignupSignal()) { Registrieren - } - @else if (this.isRegisterSignal()) { + } @else if (this.isRegisterSignal()) { Anmelden - } - @else { + } @else { Erste Schritte }

@if (this.isDisplayButtons()) {
- - + +
} @@ -35,8 +41,7 @@ pInputText id="email" formControlName="email" - aria-describedby="e-mail" - /> + aria-describedby="e-mail" />
@@ -47,8 +52,7 @@ id="password" formControlName="password" aria-describedby="password" - type="password" - /> + type="password" />
diff --git a/frontend/src/app/pages/register-root/register-root.component.scss b/frontend/src/app/pages/register-root/register-root.component.scss index 145c4fb..b306dfb 100644 --- a/frontend/src/app/pages/register-root/register-root.component.scss +++ b/frontend/src/app/pages/register-root/register-root.component.scss @@ -53,9 +53,10 @@ } .e-mail, - .password { + .password, + .terms { .label { - font-size: 1.5em; + font-size: 1; } input { @@ -63,8 +64,12 @@ } } + .terms { + padding-top: 1.33em; + } + .password { - padding-top: 3em; + padding-top: 2em; } .signup { @@ -76,3 +81,12 @@ } } } + +// .ng-invalid.ng-dirty { +// border: 1px solid red; +// } + +// .error { +// color: red; +// font-size: 12px; +// } diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index 2d4f3b1..17a759c 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -5,16 +5,22 @@ import { OnInit, WritableSignal, signal, + effect, + DestroyRef, } from '@angular/core'; import { InputTextModule } from 'primeng/inputtext'; import { FormBuilder, + FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators, } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; +import { CheckboxModule } from 'primeng/checkbox'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { debounceTime } from 'rxjs/operators'; type AuthAction = 'register' | 'signup'; @@ -27,6 +33,7 @@ type AuthAction = 'register' | 'signup'; InputTextModule, ReactiveFormsModule, ButtonModule, + CheckboxModule, ], templateUrl: './register-root.component.html', styleUrl: './register-root.component.scss', @@ -34,27 +41,91 @@ type AuthAction = 'register' | 'signup'; }) export class RegisterRootComponent implements OnInit { public form: FormGroup | undefined; + public isRegisterSignal: WritableSignal = signal(false); public isSignupSignal: WritableSignal = signal(false); public isDisplayButtons: WritableSignal = signal(true); - constructor(private readonly formBuilder: FormBuilder) {} + public emailInvalid: WritableSignal = signal(true); + public passwordInvalid: WritableSignal = signal(null); + public termsInvalid: WritableSignal = signal(null); - ngOnInit() { - this.form = this.formBuilder.group({ - email: ['', [Validators.required, Validators.email]], - password: ['', [Validators.required, Validators.minLength(6)]], + constructor( + private readonly formBuilder: FormBuilder, + private readonly destroyRef: DestroyRef + ) { + effect(() => { + if (this.form) { + if (this.isRegisterSignal()) { + this.form.addControl( + 'terms', + new FormControl(false, Validators.requiredTrue) + ); + } else { + this.form.removeControl('terms'); + } + } }); } - public toggleAction(action: AuthAction) { - const isRegister = action === 'register'; - this.isRegisterSignal.set(isRegister); - this.isSignupSignal.set(!isRegister); + public ngOnInit(): void { + this.form = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', [Validators.required, Validators.minLength(6)]], + terms: [false, [Validators.requiredTrue]], + }); + + this.form.statusChanges + .pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.updateFieldInvalidity(); + }); + } + + public toggleAction(action: AuthAction): void { + if (action === 'register') { + this.isRegisterSignal.set(true); + this.isSignupSignal.set(false); + } else { + this.isRegisterSignal.set(false); + this.isSignupSignal.set(true); + } this.isDisplayButtons.set(false); } - public onSubmit() { - console.log('signup'); + public onSubmit(): void { + this.form?.markAllAsTouched(); + this.updateFieldInvalidity(); + + if (this.isSignupSignal() && this.form?.valid) { + this.signin(); + } else if (this.isRegisterSignal() && this.form?.valid) { + this.register(); + } + } + + public DEBUG_restSignal(): void { + this.isRegisterSignal.set(false); + this.isSignupSignal.set(false); + this.isDisplayButtons.set(true); + } + + private updateFieldInvalidity(): void { + this.emailInvalid.set(this.isFieldInvalid('email')); + this.passwordInvalid.set(this.isFieldInvalid('password')); + this.termsInvalid.set(this.isFieldInvalid('terms')); + } + + private isFieldInvalid(field: string): boolean { + const formField = this.form?.get(field); + return !!formField && !formField.valid && formField.touched; + } + + private signin(): void { + console.log('Signin...'); + } + + private register(): void { + console.log('Register...'); } } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 11e0d8d..2aa826b 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -2,6 +2,9 @@ @import 'primeng/resources/themes/lara-light-blue/theme.css'; @import 'primeng/resources/primeng.css'; +// PrimeNG icons +@import 'primeicons/primeicons.css'; + html, body { height: 100%; -- 2.40.1 From e9632c35c03f862059909cb6deab88295f725ff4 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 16 May 2024 19:25:06 +0200 Subject: [PATCH 04/26] remove unused code --- .../register-root/register-root.component.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index 17a759c..81bac24 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -19,8 +19,6 @@ import { } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; import { CheckboxModule } from 'primeng/checkbox'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { debounceTime } from 'rxjs/operators'; type AuthAction = 'register' | 'signup'; @@ -41,12 +39,12 @@ type AuthAction = 'register' | 'signup'; }) export class RegisterRootComponent implements OnInit { public form: FormGroup | undefined; - + public isRegisterSignal: WritableSignal = signal(false); public isSignupSignal: WritableSignal = signal(false); public isDisplayButtons: WritableSignal = signal(true); - public emailInvalid: WritableSignal = signal(true); + public emailInvalid: WritableSignal = signal(null); public passwordInvalid: WritableSignal = signal(null); public termsInvalid: WritableSignal = signal(null); @@ -74,12 +72,6 @@ export class RegisterRootComponent implements OnInit { password: ['', [Validators.required, Validators.minLength(6)]], terms: [false, [Validators.requiredTrue]], }); - - this.form.statusChanges - .pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.updateFieldInvalidity(); - }); } public toggleAction(action: AuthAction): void { @@ -108,6 +100,10 @@ export class RegisterRootComponent implements OnInit { this.isRegisterSignal.set(false); this.isSignupSignal.set(false); this.isDisplayButtons.set(true); + + this.emailInvalid.set(false); + this.passwordInvalid.set(false); + this.termsInvalid.set(false); } private updateFieldInvalidity(): void { -- 2.40.1 From 53b5c49d5b1ba4d1df525c0b8773ef80ce954c78 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 16 May 2024 23:28:07 +0200 Subject: [PATCH 05/26] remove buggy valdator --- .../register-root.component.html | 17 +++----- .../register-root.component.scss | 12 ++---- .../register-root/register-root.component.ts | 42 ++++++------------- 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 53c9acf..e4ea6de 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -41,20 +41,18 @@ pInputText id="email" formControlName="email" - [ngClass]="{ 'ng-invalid ng-dirty': emailInvalid() }" aria-describedby="e-mail" />
- + [toggleMask]="true">
- + [toggleMask]="true">
diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index 2762ae8..09f0293 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -82,7 +82,6 @@ export class RegisterRootComponent implements OnInit { this.isRegisterSignal.set(false); this.isSignupSignal.set(true); } - this.isDisplayButtons.set(false); } diff --git a/frontend/src/app/shared/validator/email-validator.ts b/frontend/src/app/shared/validator/email-validator.ts index 5a8a7f7..2a730e5 100644 --- a/frontend/src/app/shared/validator/email-validator.ts +++ b/frontend/src/app/shared/validator/email-validator.ts @@ -3,10 +3,13 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; export function customEmailValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; + if (value.length < 4) { return { emailTooShort: true }; } + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailPattern.test(value) ? null : { emailInvalid: true }; }; } diff --git a/frontend/src/app/shared/validator/password-validator.ts b/frontend/src/app/shared/validator/password-validator.ts index abd5b0a..9c21abc 100644 --- a/frontend/src/app/shared/validator/password-validator.ts +++ b/frontend/src/app/shared/validator/password-validator.ts @@ -3,6 +3,7 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; export function customPasswordValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; + return value.length >= 8 ? null : { passwordTooShort: true }; }; } -- 2.40.1 From e09d52c9143f382eedbcc23422bd74eef63f8084 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 08:14:03 +0200 Subject: [PATCH 19/26] fix validator error, refactored view --- .../register-root.component.html | 101 +++++++++++++++++- .../register-root.component.scss | 8 ++ .../register-root/register-root.component.ts | 46 +++++--- .../app/shared/validator/email-validator.ts | 4 + .../shared/validator/password-validator.ts | 4 + 5 files changed, 147 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 98ee411..1f28c6e 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -1,7 +1,7 @@ -
+ +
+
+
+

Hi, Welcome to Ticket App.

+
+
+
+

+ @if (this.isSignupSignal()) { + Anmelden + } @else if (this.isRegisterSignal()) { + Registrieren + } @else { + Erste Schritte + } +

+ + @if (this.isDisplayButtons()) { +
+ + +
+ } + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+ @if (this.isRegisterSignal()) { +
+ +
+ } + + +
+
+
diff --git a/frontend/src/app/pages/register-root/register-root.component.scss b/frontend/src/app/pages/register-root/register-root.component.scss index b7130a4..88633cf 100644 --- a/frontend/src/app/pages/register-root/register-root.component.scss +++ b/frontend/src/app/pages/register-root/register-root.component.scss @@ -82,5 +82,13 @@ min-width: 500px; } } + .change-mask { + padding-top: 1.33em; + display: flex; + justify-content: center; + a { + cursor: pointer; + } + } } } diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index 09f0293..21001e0 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -8,7 +8,6 @@ import { effect, } from '@angular/core'; import { - AbstractControl, FormBuilder, FormControl, FormGroup, @@ -97,14 +96,16 @@ export class RegisterRootComponent implements OnInit { } } - public DEBUG_restSignal(): void { - this.isRegisterSignal.set(false); - this.isSignupSignal.set(false); - this.isDisplayButtons.set(true); + public switchMask(): void { + this.resetFormValidation(); - this.emailInvalid.set(null); - this.passwordInvalid.set(null); - this.termsInvalid.set(null); + if (this.isSignupSignal()) { + this.isSignupSignal.set(false); + this.isRegisterSignal.set(true); + } else if (this.isRegisterSignal()) { + this.isSignupSignal.set(true); + this.isRegisterSignal.set(false); + } } private initializeForm(): void { @@ -130,10 +131,10 @@ export class RegisterRootComponent implements OnInit { } private setupEmailValueChanges(): void { - const emailControl = this.form?.get('email') as AbstractControl; + const emailControl = this.form?.get('email'); - emailControl.valueChanges.subscribe((value: string) => { - if (value.length >= 4) { + emailControl?.valueChanges.subscribe((value: string) => { + if (value?.length >= 4) { emailControl.setValidators([ Validators.required, customEmailValidator(), @@ -149,10 +150,10 @@ export class RegisterRootComponent implements OnInit { } private setupPasswordValueChanges(): void { - const passwordControl = this.form?.get('password') as AbstractControl; + const passwordControl = this.form?.get('password'); - passwordControl.valueChanges.subscribe((value: string) => { - if (value.length >= 8) { + passwordControl?.valueChanges.subscribe((value: string) => { + if (value?.length >= 8) { passwordControl.setValidators([ Validators.required, customPasswordValidator(), @@ -179,6 +180,23 @@ export class RegisterRootComponent implements OnInit { }); } + private resetFormValidation(): void { + ['email', 'password', 'terms'].forEach((controlName: string) => { + this.resetControlValidation(controlName); + }); + } + + private resetControlValidation(controlName: string): void { + const control = this.form?.get(controlName); + + if (control) { + control.reset(); + control.markAsPristine(); + control.markAsUntouched(); + control.updateValueAndValidity(); + } + } + private signin(data: unknown): void { console.log(data); } diff --git a/frontend/src/app/shared/validator/email-validator.ts b/frontend/src/app/shared/validator/email-validator.ts index 2a730e5..3775c41 100644 --- a/frontend/src/app/shared/validator/email-validator.ts +++ b/frontend/src/app/shared/validator/email-validator.ts @@ -4,6 +4,10 @@ export function customEmailValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; + if (!value) { + return { emailInvalid: true }; + } + if (value.length < 4) { return { emailTooShort: true }; } diff --git a/frontend/src/app/shared/validator/password-validator.ts b/frontend/src/app/shared/validator/password-validator.ts index 9c21abc..86620dd 100644 --- a/frontend/src/app/shared/validator/password-validator.ts +++ b/frontend/src/app/shared/validator/password-validator.ts @@ -4,6 +4,10 @@ export function customPasswordValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; + if (!value) { + return { passwordTooShort: true }; + } + return value.length >= 8 ? null : { passwordTooShort: true }; }; } -- 2.40.1 From 5d2b868a3da6b75213e4d183c09230655dc3d90e Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 08:20:32 +0200 Subject: [PATCH 20/26] remove commented code --- .../register-root.component.html | 115 ------------------ 1 file changed, 115 deletions(-) diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 1f28c6e..148f752 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -1,118 +1,3 @@ -
-- 2.40.1 From c0accbbf3436d9833c6ab37552f5fb955e299c61 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 09:39:34 +0200 Subject: [PATCH 21/26] Added cors middleware --- backend/src/app.module.ts | 8 ++++- .../cors-middleware/cors.middlware.ts | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 backend/src/middleware/cors-middleware/cors.middlware.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 2b39120..137e70a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -8,6 +8,7 @@ import { SecurityHeadersMiddleware } from './middleware/security-middleware/secu import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware'; import { AuthModule } from './modules/auth-module/auth.module'; import { AccessTokenGuard } from './modules/auth-module/common/guards'; +import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware'; @Module({ imports: [ @@ -24,7 +25,12 @@ export class AppModule { configure(consumer: MiddlewareConsumer) { consumer // TODO: Redirect via Reverse Proxy all HTTP requests to HTTPS - .apply(CspMiddleware, SecurityHeadersMiddleware, HttpsRedirectMiddleware) + .apply( + CspMiddleware, + SecurityHeadersMiddleware, + HttpsRedirectMiddleware, + CorsMiddleware + ) .forRoutes({ path: '*', method: RequestMethod.ALL }); } } diff --git a/backend/src/middleware/cors-middleware/cors.middlware.ts b/backend/src/middleware/cors-middleware/cors.middlware.ts new file mode 100644 index 0000000..81ec5bd --- /dev/null +++ b/backend/src/middleware/cors-middleware/cors.middlware.ts @@ -0,0 +1,36 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class CorsMiddleware implements NestMiddleware { + constructor(private readonly configService: ConfigService) {} + + public use(req: Request, res: Response, next: NextFunction): void { + if (this.configService.get('NODE_ENV') === 'production') { + const allowedOrigin = this.configService.get('CORS_ALLOW_ORIGIN'); + + if (req.headers.origin === allowedOrigin) { + res.header('Access-Control-Allow-Origin', allowedOrigin); + res.header( + 'Access-Control-Allow-Methods', + this.configService.get('CORS_ALLOW_METHODS') + ); + res.header( + 'Access-Control-Allow-Headers', + this.configService.get('CORS_ALLOW_HEADERS') + ); + + if (req.method === 'OPTIONS') { + res.sendStatus(200); + } else { + next(); + } + } else { + res.status(403).json({ message: 'Forbidden' }); + } + } else { + next(); + } + } +} -- 2.40.1 From 0be5c36194a5b59cf56bb0c7e649bf04a3100afe Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 09:59:57 +0200 Subject: [PATCH 22/26] added basic register an login busines logic --- .../cors-middleware/cors.middlware.ts | 2 +- .../register-root/register-root.component.ts | 18 ++++++--- .../src/app/shared/service/auth.service.ts | 38 +++++++++++++++++++ frontend/src/app/shared/service/index.ts | 1 + frontend/src/app/shared/types/index.ts | 2 + .../src/app/shared/types/login-credentials.ts | 4 ++ frontend/src/app/shared/types/tokens.ts | 4 ++ frontend/src/environments/environment.ts | 3 ++ 8 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 frontend/src/app/shared/service/auth.service.ts create mode 100644 frontend/src/app/shared/service/index.ts create mode 100644 frontend/src/app/shared/types/index.ts create mode 100644 frontend/src/app/shared/types/login-credentials.ts create mode 100644 frontend/src/app/shared/types/tokens.ts diff --git a/backend/src/middleware/cors-middleware/cors.middlware.ts b/backend/src/middleware/cors-middleware/cors.middlware.ts index 81ec5bd..7a6e259 100644 --- a/backend/src/middleware/cors-middleware/cors.middlware.ts +++ b/backend/src/middleware/cors-middleware/cors.middlware.ts @@ -7,7 +7,7 @@ export class CorsMiddleware implements NestMiddleware { constructor(private readonly configService: ConfigService) {} public use(req: Request, res: Response, next: NextFunction): void { - if (this.configService.get('NODE_ENV') === 'production') { + if (this.configService.get('NODE_ENV') === 'development') { const allowedOrigin = this.configService.get('CORS_ALLOW_ORIGIN'); if (req.headers.origin === allowedOrigin) { diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index 21001e0..d25274f 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -1,4 +1,5 @@ import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, @@ -21,6 +22,8 @@ import { CheckboxModule } from 'primeng/checkbox'; import { InputTextModule } from 'primeng/inputtext'; import { PasswordModule } from 'primeng/password'; +import { AuthService } from '../../shared/service'; +import { LoginCredentials } from '../../shared/types'; import { customEmailValidator, customPasswordValidator, @@ -39,7 +42,9 @@ type AuthAction = 'register' | 'signup'; ButtonModule, CheckboxModule, PasswordModule, + HttpClientModule, ], + providers: [AuthService], templateUrl: './register-root.component.html', styleUrl: './register-root.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -53,7 +58,10 @@ export class RegisterRootComponent implements OnInit { public passwordInvalid: WritableSignal = signal(null); public termsInvalid: WritableSignal = signal(null); - public constructor(private readonly formBuilder: FormBuilder) { + public constructor( + private readonly formBuilder: FormBuilder, + private readonly authService: AuthService + ) { effect(() => { if (this.form) { if (this.isRegisterSignal()) { @@ -197,11 +205,11 @@ export class RegisterRootComponent implements OnInit { } } - private signin(data: unknown): void { - console.log(data); + private signin(logiCredentials: LoginCredentials): void { + this.authService.signin(logiCredentials); } - private register(data: unknown): void { - console.log(data); + private register(logiCredentials: LoginCredentials): void { + this.authService.signup(logiCredentials); } } diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts new file mode 100644 index 0000000..f73183c --- /dev/null +++ b/frontend/src/app/shared/service/auth.service.ts @@ -0,0 +1,38 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +import { environment } from '../../../environments/environment'; +import { LoginCredentials, Tokens } from '../types'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private isAuthenticated: boolean = false; + private access_token: string | null = null; + private refresh_token: string | null = null; + + public constructor( + private readonly httpClient: HttpClient, + private readonly router: Router + ) {} + + public signin(credentials: LoginCredentials): void { + this.httpClient + .post(environment.api.base + '/api/auth/signin', credentials) + .subscribe((response: Tokens) => { + this.access_token = response.access_token; + this.refresh_token = response.refresh_token; + }); + } + + public signup(credentials: LoginCredentials): void { + this.httpClient + .post(environment.api.base + '/api/auth/signup', credentials) + .subscribe((response: Tokens) => { + this.access_token = response.access_token; + this.refresh_token = response.refresh_token; + }); + } +} diff --git a/frontend/src/app/shared/service/index.ts b/frontend/src/app/shared/service/index.ts new file mode 100644 index 0000000..2a719d1 --- /dev/null +++ b/frontend/src/app/shared/service/index.ts @@ -0,0 +1 @@ +export * from './auth.service'; diff --git a/frontend/src/app/shared/types/index.ts b/frontend/src/app/shared/types/index.ts new file mode 100644 index 0000000..c95db91 --- /dev/null +++ b/frontend/src/app/shared/types/index.ts @@ -0,0 +1,2 @@ +export * from './login-credentials'; +export * from './tokens'; diff --git a/frontend/src/app/shared/types/login-credentials.ts b/frontend/src/app/shared/types/login-credentials.ts new file mode 100644 index 0000000..e37f277 --- /dev/null +++ b/frontend/src/app/shared/types/login-credentials.ts @@ -0,0 +1,4 @@ +export type LoginCredentials = { + email: string; + password: string; +}; diff --git a/frontend/src/app/shared/types/tokens.ts b/frontend/src/app/shared/types/tokens.ts new file mode 100644 index 0000000..1c0a510 --- /dev/null +++ b/frontend/src/app/shared/types/tokens.ts @@ -0,0 +1,4 @@ +export type Tokens = { + access_token: string; + refresh_token: string; +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 2e568c3..de4bacf 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -1,5 +1,8 @@ export const environment = { production: false, + api: { + base: 'http://localhost:3000', + }, oauth: { clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA', redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth', -- 2.40.1 From bdc9ec3ee0e5ccb8d70634823450b3a05abca8b6 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 13:54:46 +0200 Subject: [PATCH 23/26] added auth service --- .../src/app/shared/service/auth.service.ts | 59 +++++++++++++++---- .../shared/service/base-storage.service.ts | 33 +++++++++++ .../app/shared/service/encryption.service.ts | 15 +++++ frontend/src/app/shared/service/index.ts | 2 + .../shared/service/local-storage.service.ts | 12 ++++ .../shared/service/sanitization.service.ts | 7 +++ .../shared/service/session-storage.service.ts | 12 ++++ 7 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/shared/service/base-storage.service.ts create mode 100644 frontend/src/app/shared/service/encryption.service.ts create mode 100644 frontend/src/app/shared/service/local-storage.service.ts create mode 100644 frontend/src/app/shared/service/sanitization.service.ts create mode 100644 frontend/src/app/shared/service/session-storage.service.ts diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index f73183c..4763c8b 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -1,38 +1,77 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; + +import { BehaviorSubject } from 'rxjs'; import { environment } from '../../../environments/environment'; import { LoginCredentials, Tokens } from '../types'; +import { LocalStorageService } from './local-storage.service'; +import { SessionStorageService } from './session-storage.service'; + @Injectable({ providedIn: 'root', }) export class AuthService { - private isAuthenticated: boolean = false; + private readonly path: string = '/api/auth'; private access_token: string | null = null; private refresh_token: string | null = null; + private isAuthenticated$: BehaviorSubject = + new BehaviorSubject(false); public constructor( private readonly httpClient: HttpClient, - private readonly router: Router + private readonly localStorageService: LocalStorageService, + private readonly sessionStorageService: SessionStorageService ) {} public signin(credentials: LoginCredentials): void { this.httpClient - .post(environment.api.base + '/api/auth/signin', credentials) + .post(environment.api.base + `${this.path}/signin`, credentials) .subscribe((response: Tokens) => { - this.access_token = response.access_token; - this.refresh_token = response.refresh_token; + this.handleSuccess(response); }); } public signup(credentials: LoginCredentials): void { this.httpClient - .post(environment.api.base + '/api/auth/signup', credentials) + .post(environment.api.base + `${this.path}/signup`, credentials) .subscribe((response: Tokens) => { - this.access_token = response.access_token; - this.refresh_token = response.refresh_token; + // The checked accept terms should be saved with a timestamp in the db + this.handleSuccess(response); }); } + + public signout(): void { + this.access_token = null; + this.refresh_token = null; + this.localStorageService.removeItem('access_token'); + this.sessionStorageService.removeItem('refresh_token'); + this.isAuthenticated$.next(false); + } + + public refreshToken(): void { + const headers = new HttpHeaders().set( + 'Authorization', + 'Bearer ' + this.refresh_token + ); + + this.httpClient + .post( + environment.api.base + `${this.path}/refresh`, + {}, + { headers: headers } + ) + .subscribe((response: Tokens) => { + this.handleSuccess(response); + }); + } + + private handleSuccess(tokens: Tokens): void { + this.access_token = tokens.access_token; + this.refresh_token = tokens.refresh_token; + this.localStorageService.setItem('access_token', tokens.access_token); + this.sessionStorageService.setItem('refresh_token', tokens.refresh_token); + this.isAuthenticated$.next(true); + } } diff --git a/frontend/src/app/shared/service/base-storage.service.ts b/frontend/src/app/shared/service/base-storage.service.ts new file mode 100644 index 0000000..24dcb19 --- /dev/null +++ b/frontend/src/app/shared/service/base-storage.service.ts @@ -0,0 +1,33 @@ +import { EncryptionService } from './encryption.service'; +import { SanitizationService } from './sanitization.service'; + +export abstract class BaseStorageService { + protected abstract getStorage(): Storage; + + public setItem(key: string, value: T): void { + const sanitizedValue = SanitizationService.sanitize(JSON.stringify(value)); + const encryptedValue = EncryptionService.encrypt(sanitizedValue); + + this.getStorage().setItem(key, encryptedValue); + } + + public getItem(key: string): T | null { + const encryptedValue = this.getStorage().getItem(key); + + if (encryptedValue) { + const decryptedValue = EncryptionService.decrypt(encryptedValue); + const sanitizedValue = SanitizationService.sanitize(decryptedValue); + + return JSON.parse(sanitizedValue) as T; + } + return null; + } + + public removeItem(key: string): void { + this.getStorage().removeItem(key); + } + + public clear(): void { + this.getStorage().clear(); + } +} diff --git a/frontend/src/app/shared/service/encryption.service.ts b/frontend/src/app/shared/service/encryption.service.ts new file mode 100644 index 0000000..8823749 --- /dev/null +++ b/frontend/src/app/shared/service/encryption.service.ts @@ -0,0 +1,15 @@ +import * as CryptoJS from 'crypto-js'; + +import { environment } from '../../../environments/environment'; + +export class EncryptionService { + private static readonly key: string = environment.security.encryptionKey; + + public static encrypt(data: string): string { + return CryptoJS.AES.encrypt(data, this.key).toString(); + } + + public static decrypt(data: string): string { + return CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8); + } +} diff --git a/frontend/src/app/shared/service/index.ts b/frontend/src/app/shared/service/index.ts index 2a719d1..f11d653 100644 --- a/frontend/src/app/shared/service/index.ts +++ b/frontend/src/app/shared/service/index.ts @@ -1 +1,3 @@ export * from './auth.service'; +export * from './local-storage.service'; +export * from './session-storage.service'; diff --git a/frontend/src/app/shared/service/local-storage.service.ts b/frontend/src/app/shared/service/local-storage.service.ts new file mode 100644 index 0000000..34fd729 --- /dev/null +++ b/frontend/src/app/shared/service/local-storage.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core'; + +import { BaseStorageService } from './base-storage.service'; + +@Injectable({ + providedIn: 'root', +}) +export class LocalStorageService extends BaseStorageService { + protected getStorage(): Storage { + return localStorage; + } +} diff --git a/frontend/src/app/shared/service/sanitization.service.ts b/frontend/src/app/shared/service/sanitization.service.ts new file mode 100644 index 0000000..a787262 --- /dev/null +++ b/frontend/src/app/shared/service/sanitization.service.ts @@ -0,0 +1,7 @@ +import DOMPurify from 'dompurify'; + +export class SanitizationService { + public static sanitize(data: string): string { + return DOMPurify.sanitize(data); + } +} diff --git a/frontend/src/app/shared/service/session-storage.service.ts b/frontend/src/app/shared/service/session-storage.service.ts new file mode 100644 index 0000000..c31601b --- /dev/null +++ b/frontend/src/app/shared/service/session-storage.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core'; + +import { BaseStorageService } from './base-storage.service'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionStorageService extends BaseStorageService { + protected getStorage(): Storage { + return sessionStorage; + } +} -- 2.40.1 From 38d127c948c9fd4d34ff78cc8938e0c81777dd9b Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 13:55:17 +0200 Subject: [PATCH 24/26] refactored html --- .../register-root.component.html | 117 ++++++++++-------- frontend/src/environments/environment.ts | 3 + 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 148f752..0e3bc6d 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -6,16 +6,16 @@

- @if (this.isSignupSignal()) { + @if (isSignupSignal()) { Anmelden - } @else if (this.isRegisterSignal()) { + } @else if (isRegisterSignal()) { Registrieren } @else { Erste Schritte }

- @if (this.isDisplayButtons()) { + @if (isDisplayButtons()) {
-
- + + + + } +
+ }
diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index de4bacf..f1a8c8a 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -3,6 +3,9 @@ export const environment = { api: { base: 'http://localhost:3000', }, + security: { + encryptionKey: 'my-secret', + }, oauth: { clinetId: 'app_FLXnxSBnnaKkXoYCgk3J62iA', redirectUri: 'https://commonly-hot-airedale.ngrok-free.app/oauth', -- 2.40.1 From b14a4a38a0118d7e9ce00f2849eadf483f08251a Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 20 May 2024 13:55:30 +0200 Subject: [PATCH 25/26] update package.json --- frontend/package.json | 4 ++++ frontend/pnpm-lock.yaml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 151b7f0..48c08f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,9 @@ "@angular/platform-browser": "^17.3.0", "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", + "@types/dompurify": "^3.0.5", + "crypto-js": "^4.2.0", + "dompurify": "^3.1.3", "primeng": "^17.11.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -45,6 +48,7 @@ "@stylistic/eslint-plugin": "^2.1.0", "@stylistic/eslint-plugin-migrate": "^2.1.0", "@stylistic/eslint-plugin-ts": "^2.1.0", + "@types/crypto-js": "^4.2.2", "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/parser": "6.19.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b51b3d7..0e4e337 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -29,6 +29,15 @@ dependencies: '@angular/router': specifier: ^17.3.0 version: 17.3.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/platform-browser@17.3.0)(rxjs@7.8.1) + '@types/dompurify': + specifier: ^3.0.5 + version: 3.0.5 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dompurify: + specifier: ^3.1.3 + version: 3.1.3 primeng: specifier: ^17.11.0 version: 17.11.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/forms@17.3.0)(rxjs@7.8.1)(zone.js@0.14.4) @@ -76,6 +85,9 @@ devDependencies: '@stylistic/eslint-plugin-ts': specifier: ^2.1.0 version: 2.1.0(eslint@8.57.0)(typescript@5.4.2) + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -3231,6 +3243,16 @@ packages: '@types/node': 20.11.27 dev: true + /@types/crypto-js@4.2.2: + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + dev: true + + /@types/dompurify@3.0.5: + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + dependencies: + '@types/trusted-types': 2.0.7 + dev: false + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -3400,6 +3422,10 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} dev: true + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: false + /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: @@ -4710,6 +4736,10 @@ packages: which: 2.0.2 dev: true + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + /css-loader@6.10.0(webpack@5.90.3): resolution: {integrity: sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==} engines: {node: '>= 12.13.0'} @@ -4998,6 +5028,10 @@ packages: domelementtype: 2.3.0 dev: true + /dompurify@3.1.3: + resolution: {integrity: sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng==} + dev: false + /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dependencies: -- 2.40.1 From e308a7bace0b5836b456224c87d4b1b52ac4edc1 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 21 May 2024 21:21:33 +0200 Subject: [PATCH 26/26] added intercepter + auto login --- frontend/src/app/app.component.ts | 10 ++- frontend/src/app/app.config.ts | 3 + .../shared/interceptors/auth.interceptor.ts | 51 ++++++++++++++ frontend/src/app/shared/interceptors/index.ts | 0 .../src/app/shared/service/auth.service.ts | 67 +++++++++++++------ 5 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 frontend/src/app/shared/interceptors/auth.interceptor.ts create mode 100644 frontend/src/app/shared/interceptors/index.ts diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 76f2faf..2ffb389 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,11 +1,15 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; + +import { AuthService } from './shared/service'; @Component({ selector: 'app-root', standalone: true, - providers: [], + providers: [AuthService], imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.scss', }) -export class AppComponent {} +export class AppComponent { + private readonly authService: AuthService = inject(AuthService); +} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 200a054..9b95e78 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,11 +1,14 @@ +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; +import { AuthInterceptor } from './shared/interceptors/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ + provideHttpClient(withInterceptors([AuthInterceptor])), provideRouter(routes, withComponentInputBinding()), provideAnimations(), ], diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.ts b/frontend/src/app/shared/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..7259b6c --- /dev/null +++ b/frontend/src/app/shared/interceptors/auth.interceptor.ts @@ -0,0 +1,51 @@ +import { + HttpErrorResponse, + HttpEvent, + HttpHandlerFn, + HttpInterceptorFn, + HttpRequest, +} from '@angular/common/http'; +import { inject } from '@angular/core'; + +import { Observable, catchError, switchMap, throwError } from 'rxjs'; + +import { AuthService } from '../service'; +import { Tokens } from '../types'; + +export const AuthInterceptor: HttpInterceptorFn = ( + request: HttpRequest, + next: HttpHandlerFn +): Observable> => { + const authService: AuthService = inject(AuthService); + const accessToken: string | null = authService.access_token; + + if (accessToken) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${accessToken}`, + }, + }); + } + return next(request).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + return authService.refreshToken().pipe( + switchMap((tokens: Tokens) => { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${tokens.access_token}`, + }, + }); + return next(request); + }), + catchError((refreshError) => { + authService.signout(); + return throwError(() => new Error(refreshError)); + }) + ); + } + + return throwError(() => new Error()); + }) + ); +}; diff --git a/frontend/src/app/shared/interceptors/index.ts b/frontend/src/app/shared/interceptors/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index 4763c8b..a67ef5f 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable, tap } from 'rxjs'; import { environment } from '../../../environments/environment'; import { LoginCredentials, Tokens } from '../types'; @@ -13,21 +13,27 @@ import { SessionStorageService } from './session-storage.service'; providedIn: 'root', }) export class AuthService { - private readonly path: string = '/api/auth'; - private access_token: string | null = null; - private refresh_token: string | null = null; - private isAuthenticated$: BehaviorSubject = + private readonly _path: string = '/api/auth'; + private _access_token: string | null = null; + private _refresh_token: string | null = null; + private _isAuthenticated$: BehaviorSubject = new BehaviorSubject(false); + public get access_token(): string | null { + return this._access_token; + } + public constructor( private readonly httpClient: HttpClient, private readonly localStorageService: LocalStorageService, private readonly sessionStorageService: SessionStorageService - ) {} + ) { + this.autoLogin(); + } public signin(credentials: LoginCredentials): void { this.httpClient - .post(environment.api.base + `${this.path}/signin`, credentials) + .post(environment.api.base + `${this._path}/signin`, credentials) .subscribe((response: Tokens) => { this.handleSuccess(response); }); @@ -35,43 +41,60 @@ export class AuthService { public signup(credentials: LoginCredentials): void { this.httpClient - .post(environment.api.base + `${this.path}/signup`, credentials) + .post(environment.api.base + `${this._path}/signup`, credentials) .subscribe((response: Tokens) => { - // The checked accept terms should be saved with a timestamp in the db + //TODO The checked accept terms should be saved with a timestamp in the db this.handleSuccess(response); }); } public signout(): void { - this.access_token = null; - this.refresh_token = null; + this._access_token = null; + this._refresh_token = null; this.localStorageService.removeItem('access_token'); this.sessionStorageService.removeItem('refresh_token'); - this.isAuthenticated$.next(false); + this._isAuthenticated$.next(false); } - public refreshToken(): void { + public autoLogin(): void { + const storedAccessToken: string | null = + this.localStorageService.getItem('access_token'); + const storedRefreshToken: string | null = + this.sessionStorageService.getItem('refresh_token'); + + if (storedAccessToken && storedRefreshToken) { + this._refresh_token = storedRefreshToken; + this._isAuthenticated$.next(true); + //TODO Validate tokens with backend or decode JWT to check expiration + } else { + this.signout(); + } + } + + public refreshToken(): Observable { const headers = new HttpHeaders().set( 'Authorization', - 'Bearer ' + this.refresh_token + 'Bearer ' + this._refresh_token ); - this.httpClient + return this.httpClient .post( - environment.api.base + `${this.path}/refresh`, + environment.api.base + `${this._path}/refresh`, {}, { headers: headers } ) - .subscribe((response: Tokens) => { - this.handleSuccess(response); - }); + .pipe( + tap((response: Tokens) => { + this.handleSuccess(response); + }) + ); } private handleSuccess(tokens: Tokens): void { - this.access_token = tokens.access_token; - this.refresh_token = tokens.refresh_token; + this._access_token = tokens.access_token; + this._refresh_token = tokens.refresh_token; this.localStorageService.setItem('access_token', tokens.access_token); this.sessionStorageService.setItem('refresh_token', tokens.refresh_token); - this.isAuthenticated$.next(true); + this._isAuthenticated$.next(true); } } -- 2.40.1