From 0b9b648861f3d985003f72938c5b199e0741cfef Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Tue, 1 Aug 2023 01:05:45 -0300 Subject: [PATCH] Implementation of Better Error Handling on Login --- .../callback/callback.component.ts | 22 ++-- .../error-box/error-box.component.css | 11 ++ .../error-box/error-box.component.html | 3 + .../error-box/error-box.component.spec.ts | 23 ++++ .../error-box/error-box.component.ts | 14 +++ .../header-popup/login/login.component.css | 6 +- .../header-popup/login/login.component.html | 109 +++++++++-------- .../header-popup/login/login.component.ts | 114 +++++++++++++++--- src/app/header/header.module.ts | 9 +- src/app/shared/auth/auth.service.ts | 58 +++++---- .../components/popup/popup.component.css | 4 +- .../components/popup/popup.component.ts | 6 +- src/app/shared/shared.module.ts | 4 +- 13 files changed, 269 insertions(+), 114 deletions(-) create mode 100644 src/app/header/header-popup/error-box/error-box.component.css create mode 100644 src/app/header/header-popup/error-box/error-box.component.html create mode 100644 src/app/header/header-popup/error-box/error-box.component.spec.ts create mode 100644 src/app/header/header-popup/error-box/error-box.component.ts diff --git a/src/app/header/header-popup/callback/callback.component.ts b/src/app/header/header-popup/callback/callback.component.ts index a7f3d46..9a5db0f 100644 --- a/src/app/header/header-popup/callback/callback.component.ts +++ b/src/app/header/header-popup/callback/callback.component.ts @@ -19,10 +19,16 @@ export class CallbackComponent implements OnInit { let auth: 'google' | 'github' = p['auth']; - if (auth === 'google') { - this.loginGoogle(p); - } else if (auth === 'github') { - this.loginGithub(p); + switch (auth) { + case "github": + this.authService.loginGithubUser(p) + break; + case "google": + this.authService.loginGoogleUser(p) + break; + default: + console.log(`Unimplemented auth: ${auth}`) + break; } this.router.navigate(['/home']) @@ -30,12 +36,4 @@ export class CallbackComponent implements OnInit { }) } - private loginGoogle(p: any) { - this.authService.loginGoogleUser(p) - } - - private loginGithub(p: any) { - this.authService.loginGithubUser(p) - } - } diff --git a/src/app/header/header-popup/error-box/error-box.component.css b/src/app/header/header-popup/error-box/error-box.component.css new file mode 100644 index 0000000..b15d3ad --- /dev/null +++ b/src/app/header/header-popup/error-box/error-box.component.css @@ -0,0 +1,11 @@ +.error-box { + background-color: #ff00001a; + min-width: 250px; + display: flex; + border-radius: 5px; + padding: 25px; + margin: 0 auto; + color: #ff0000; + font-size: 14px; + font-weight: 500; +} diff --git a/src/app/header/header-popup/error-box/error-box.component.html b/src/app/header/header-popup/error-box/error-box.component.html new file mode 100644 index 0000000..dc5f057 --- /dev/null +++ b/src/app/header/header-popup/error-box/error-box.component.html @@ -0,0 +1,3 @@ +
+ {{errorMessage}} +
diff --git a/src/app/header/header-popup/error-box/error-box.component.spec.ts b/src/app/header/header-popup/error-box/error-box.component.spec.ts new file mode 100644 index 0000000..638a608 --- /dev/null +++ b/src/app/header/header-popup/error-box/error-box.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorBoxComponent } from './error-box.component'; + +describe('ErrorBoxComponent', () => { + let component: ErrorBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ErrorBoxComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ErrorBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/header/header-popup/error-box/error-box.component.ts b/src/app/header/header-popup/error-box/error-box.component.ts new file mode 100644 index 0000000..89ec9ba --- /dev/null +++ b/src/app/header/header-popup/error-box/error-box.component.ts @@ -0,0 +1,14 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'app-error-box', + templateUrl: './error-box.component.html', + styleUrls: ['./error-box.component.css'] +}) +export class ErrorBoxComponent { + @Input() + errorMessage: string|null = "Error, please try again later." + + constructor() { } + +} diff --git a/src/app/header/header-popup/login/login.component.css b/src/app/header/header-popup/login/login.component.css index df70563..862da27 100644 --- a/src/app/header/header-popup/login/login.component.css +++ b/src/app/header/header-popup/login/login.component.css @@ -113,12 +113,14 @@ transition: .3s; } + + @media (min-width:767px) { .authentication-container { min-width: 630px; } - + .authentication-body { all: unset; justify-content: space-around; @@ -145,4 +147,4 @@ height: 100%; } -} \ No newline at end of file +} diff --git a/src/app/header/header-popup/login/login.component.html b/src/app/header/header-popup/login/login.component.html index e358d43..2c36c3c 100644 --- a/src/app/header/header-popup/login/login.component.html +++ b/src/app/header/header-popup/login/login.component.html @@ -1,57 +1,66 @@ - - {{errorMessage}} - -
-
-
-
-
- - - -
-
- - - -
- +
+
+
+
+
+
+ - -
-
-
-
-
- - + +
+ -
\ No newline at end of file + diff --git a/src/app/header/header-popup/login/login.component.ts b/src/app/header/header-popup/login/login.component.ts index 115c0e7..96d1373 100644 --- a/src/app/header/header-popup/login/login.component.ts +++ b/src/app/header/header-popup/login/login.component.ts @@ -3,12 +3,13 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { faLock, faUser } from '@fortawesome/free-solid-svg-icons'; -import { Subscription } from 'rxjs'; +import {Subscription, timeout} from 'rxjs'; import { AuthService } from 'src/app/shared/auth/auth.service'; import { HttpError } from 'src/app/shared/model/httpError/httpError.model'; import HttpErrorChecker from 'src/app/shared/model/httpError/httpErrorChecker'; import UserChecker from 'src/app/shared/model/user/user.checker'; import { User } from 'src/app/shared/model/user/user.model'; +import {animate, animateChild, group, query, state, style, transition, trigger} from "@angular/animations"; const GOOGLE_LOGO_SVG = "assets/img/providers/google.svg"; @@ -17,18 +18,81 @@ const GITHUB_LOGO_SVG = "assets/img/providers/github.svg"; @Component({ selector: 'app-login', templateUrl: './login.component.html', - styleUrls: ['./login.component.css'] + styleUrls: ['./login.component.css'], + animations: [ + trigger('resizeContainerForErrorMessage', [ + state('hide', + style({ + height: '100px', + width: '370px', + }) + ), + transition( + 'show => hide', + group([ + query( + "@*", + animateChild(), + { optional: true } + ), + animate('1s ease') + ]) + ) + ]), + trigger('showErrorMessage', [ + state('show', + style({ + opacity: 1, + height: '100px', + width: '340px', + }) + ), + state('hide', + style({ + opacity: 0, + height: '0px', + width: '0px', + }) + ), + transition( + '* => show', + animate( + '500ms ease-in' + ) + ), + ]), + trigger('hideAuthContainer', [ + state('hide', + style({ + opacity: 0, + }) + ), + transition( + 'show => hide', + group([ + query( + "@*", + animateChild(), + { optional: true } + ), + animate( + '250ms ease-out' + ) + ]) + ) + ]), + ] }) export class LoginComponent implements OnInit, AfterViewInit, OnDestroy { @Input() - state: boolean = false; + state: boolean = false; @Input() - ignoreClickOutside!: HTMLDivElement[]; + ignoreClickOutside!: HTMLDivElement[]; @Output() - stateChange = new EventEmitter(); + stateChange = new EventEmitter(); popupState = false; @@ -38,6 +102,8 @@ export class LoginComponent implements OnInit, AfterViewInit, OnDestroy { errorMessage!: string | null; + isShowErrorMessage = false; + _userIcon = faUser; _passwordIcon = faLock; @@ -47,14 +113,14 @@ export class LoginComponent implements OnInit, AfterViewInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer) { - this.matIconRegistry.addSvgIcon( - "google-logo", - this.domSanitizer.bypassSecurityTrustResourceUrl(GOOGLE_LOGO_SVG) - ); - this.matIconRegistry.addSvgIcon( - "github-logo", - this.domSanitizer.bypassSecurityTrustResourceUrl(GITHUB_LOGO_SVG) - ); + this.matIconRegistry.addSvgIcon( + "google-logo", + this.domSanitizer.bypassSecurityTrustResourceUrl(GOOGLE_LOGO_SVG) + ); + this.matIconRegistry.addSvgIcon( + "github-logo", + this.domSanitizer.bypassSecurityTrustResourceUrl(GITHUB_LOGO_SVG) + ); } ngOnInit(): void { @@ -89,7 +155,6 @@ export class LoginComponent implements OnInit, AfterViewInit, OnDestroy { password: this.loginForm.controls['password'].value } this.authService.login(user); - } onGoogleLogin() { @@ -114,4 +179,25 @@ export class LoginComponent implements OnInit, AfterViewInit, OnDestroy { this.loginForm.reset(); } + public showErrorMessage(): string { + if (this.isShowErrorMessage) { + return "show"; + } + return "hide"; + } + + public hideErrorMessage(): string { + if (!!this.errorMessage) { + return "hide"; + } + return "show"; + } + + hideAuthContainer(event: any) { + if (event.toState === "hide") { + event.element.style.display = "none"; + this.isShowErrorMessage = true; + } + } + } diff --git a/src/app/header/header.module.ts b/src/app/header/header.module.ts index 5eeaa7b..28e1946 100644 --- a/src/app/header/header.module.ts +++ b/src/app/header/header.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import {CommonModule, NgOptimizedImage} from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { HeaderComponent } from './header.component'; @@ -15,6 +15,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SignupComponent } from './header-popup/signup/signup.component'; import { CallbackComponent } from './header-popup/callback/callback.component'; import {MatIconModule} from '@angular/material/icon'; +import { ErrorBoxComponent } from './header-popup/error-box/error-box.component'; @@ -27,7 +28,8 @@ import {MatIconModule} from '@angular/material/icon'; HeaderDropdownComponent, LoginComponent, SignupComponent, - CallbackComponent + CallbackComponent, + ErrorBoxComponent, ], imports: [ CommonModule, @@ -37,7 +39,8 @@ import {MatIconModule} from '@angular/material/icon'; FormsModule, ReactiveFormsModule, MatIconModule, - SharedModule + SharedModule, + NgOptimizedImage ], exports: [ HeaderComponent, HeaderSliderComponent, diff --git a/src/app/shared/auth/auth.service.ts b/src/app/shared/auth/auth.service.ts index 1d050bb..178d534 100644 --- a/src/app/shared/auth/auth.service.ts +++ b/src/app/shared/auth/auth.service.ts @@ -1,8 +1,8 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { first, firstValueFrom, Observable, Subject, take } from 'rxjs'; +import {first, firstValueFrom, Observable, of, Subject, take} from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; -import { param } from 'ts-interface-checker'; import { HttpError } from '../model/httpError/httpError.model'; import { Token } from '../model/token/token.model'; import { User } from '../model/user/user.model'; @@ -25,11 +25,11 @@ export class AuthService { login(userAuthAtempt: User): void { this.validateUser(this.loginUser(userAuthAtempt)); } - + googleLogin() { window.open(this.BACKEND_OAUTH_PATH + '/oauth2/authorization/google', '_self'); } - + githubLogin() { window.open(this.BACKEND_OAUTH_PATH + '/oauth2/authorization/github', '_self'); } @@ -95,9 +95,9 @@ export class AuthService { return this.http.get( this.BACKEND_OAUTH_PATH + '/login/oauth2/code/google', - { + { withCredentials: true, - params: params + params: params }, ).pipe( first() @@ -114,9 +114,9 @@ export class AuthService { return this.http.get( this.BACKEND_OAUTH_PATH + '/login/oauth2/code/github', - { + { withCredentials: true, - params: params + params: params }, ).pipe( first() @@ -133,18 +133,6 @@ export class AuthService { ) } - private validateUser(userAuthAtempt: Observable) { - userAuthAtempt.subscribe({ - next: userAuthentication => { - this.userAuthenticated = userAuthentication; - this.authSubject.next(this.userAuthenticated); - }, - error: err => { - this.authSubject.next(err.error); - } - }); - } - private refreshAccessToken() { return firstValueFrom(this.http.post( this.BACKEND_PATH + "/user/login/refresh", @@ -157,19 +145,37 @@ export class AuthService { return this.http.get( this.BACKEND_PATH + '/session/validate', { withCredentials: true } - ).pipe( - first() ); } private destroySessions() { - return this.http.post( + return this.http.delete( this.BACKEND_PATH + '/session/destroy', - {}, { withCredentials: true } - ).pipe( - take(1) ); } + private validateUser(userAuthAtempt: Observable) { + userAuthAtempt.pipe( + catchError(error => { + if (error.status == 0) { + return of({ + title: "Service Unavailable", + status: 500, + details: "Service Unavailable, please try again later.", + developerMessage: "Service Unavailable, please try again later.", + timestamp: new Date().toISOString() + }); + } + return of(error.error); + }), + first() + ).subscribe({ + next: userAuthentication => { + this.userAuthenticated = userAuthentication; + this.authSubject.next(this.userAuthenticated); + } + }); + } + } diff --git a/src/app/shared/components/popup/popup.component.css b/src/app/shared/components/popup/popup.component.css index b502aa0..9f1c197 100644 --- a/src/app/shared/components/popup/popup.component.css +++ b/src/app/shared/components/popup/popup.component.css @@ -64,7 +64,7 @@ .popup-body { height: calc(100% - 120px); - padding: 0px 60px; + padding: 0px 30px; width: 100%; margin: 0; } @@ -80,4 +80,4 @@ max-width: unset; } -} \ No newline at end of file +} diff --git a/src/app/shared/components/popup/popup.component.ts b/src/app/shared/components/popup/popup.component.ts index 9e8f6da..24502f2 100644 --- a/src/app/shared/components/popup/popup.component.ts +++ b/src/app/shared/components/popup/popup.component.ts @@ -17,7 +17,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewEncapsulation } '* => show', group([ query( - "@*", + "@*", animateChild(), { optional: true } ), @@ -28,7 +28,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewEncapsulation } 'show => hide', group([ query( - "@*", + "@*", animateChild(), { optional: true } ), @@ -51,7 +51,7 @@ export class PopupComponent { constructor() { } - get popupState(): string { + get popupState(): string { return this.state ? 'show' : 'hide'; } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 29f158a..eb36fc2 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -8,10 +8,10 @@ import { PopupComponent } from './components/popup/popup.component'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; @NgModule({ - declarations: [ + declarations: [ ClickedOutsideDirective, SliderItemComponent, - PopupComponent + PopupComponent, ], imports: [ CommonModule,