From 9f40a6c78266a1c8db01acf933505e8af7b2dbe1 Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Tue, 5 Dec 2023 14:31:07 +0100 Subject: [PATCH] Implementation of login and logout mechanisms. --- sportshub-gui/package-lock.json | 13 ++++++ sportshub-gui/package.json | 3 +- sportshub-gui/src/app/app.component.scss | 8 ++++ sportshub-gui/src/app/app.routes.ts | 4 ++ .../app/components/login/login.component.ts | 11 ++--- .../src/app/components/login/login.module.ts | 4 ++ .../src/app/components/login/login.service.ts | 37 +++++++++++++++ .../components/logout/logout.component.html | 2 + .../components/logout/logout.component.scss | 6 +++ .../app/components/logout/logout.component.ts | 15 +++++++ .../app/components/logout/logout.module.ts | 29 ++++++++++++ .../app/components/logout/logout.service.ts | 16 +++++++ .../core/services/authentication.service.ts | 45 +++++++++++++++++++ .../src/app/core/services/message.service.ts | 15 +++++++ .../src/app/header/header.component.html | 9 +++- .../src/app/header/header.component.scss | 8 +++- .../src/app/header/header.component.ts | 16 ++++++- 17 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 sportshub-gui/src/app/components/logout/logout.component.html create mode 100644 sportshub-gui/src/app/components/logout/logout.component.scss create mode 100644 sportshub-gui/src/app/components/logout/logout.component.ts create mode 100644 sportshub-gui/src/app/components/logout/logout.module.ts create mode 100644 sportshub-gui/src/app/components/logout/logout.service.ts create mode 100644 sportshub-gui/src/app/core/services/authentication.service.ts create mode 100644 sportshub-gui/src/app/core/services/message.service.ts diff --git a/sportshub-gui/package-lock.json b/sportshub-gui/package-lock.json index 6d11879..868d7e5 100644 --- a/sportshub-gui/package-lock.json +++ b/sportshub-gui/package-lock.json @@ -18,6 +18,7 @@ "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", + "ngx-cookie-service": "^17.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.2" @@ -13096,6 +13097,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ngx-cookie-service": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-17.0.0.tgz", + "integrity": "sha512-5mCitsrcUPBlHSzZH1NNKGTwd9NDVq1AD3lXN6XxqtgL+aFRguNyVaEUYADlx37JBnI1LAnN6PE6Z+wK9CpRvw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/common": "^17.0.0", + "@angular/core": "^17.0.0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", diff --git a/sportshub-gui/package.json b/sportshub-gui/package.json index 18219cd..d49333e 100644 --- a/sportshub-gui/package.json +++ b/sportshub-gui/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", + "ngx-cookie-service": "^17.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.2" @@ -46,4 +47,4 @@ "/src/setupJest.ts" ] } -} \ No newline at end of file +} diff --git a/sportshub-gui/src/app/app.component.scss b/sportshub-gui/src/app/app.component.scss index e69de29..0e73e7f 100644 --- a/sportshub-gui/src/app/app.component.scss +++ b/sportshub-gui/src/app/app.component.scss @@ -0,0 +1,8 @@ +:host { + display: flex; + flex-direction: column; + + app-header { + margin-bottom: 1em; + } +} \ No newline at end of file diff --git a/sportshub-gui/src/app/app.routes.ts b/sportshub-gui/src/app/app.routes.ts index 43969bd..27102bb 100644 --- a/sportshub-gui/src/app/app.routes.ts +++ b/sportshub-gui/src/app/app.routes.ts @@ -8,5 +8,9 @@ export const routes: Routes = [ { path: 'login', loadChildren: () => import('./components/login/login.module').then(module => module.LoginModule) + }, + { + path: 'logout', + loadChildren: () => import('./components/logout/logout.module').then(module => module.LogoutModule) } ]; diff --git a/sportshub-gui/src/app/components/login/login.component.ts b/sportshub-gui/src/app/components/login/login.component.ts index 136623d..6d94f64 100644 --- a/sportshub-gui/src/app/components/login/login.component.ts +++ b/sportshub-gui/src/app/components/login/login.component.ts @@ -3,6 +3,7 @@ import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; import {UserRestService} from "../../core/rest-services/user.rest-service"; import {LoginRequest} from "../../core/model/login-request"; import {MatSnackBar} from "@angular/material/snack-bar"; +import {LoginService} from "./login.service"; @Component({ selector: 'app-login', @@ -15,6 +16,7 @@ export class LoginComponent { constructor( private formBuilder: FormBuilder, + private loginService: LoginService, private matSnackBar: MatSnackBar, private userRestService: UserRestService ) { @@ -25,14 +27,7 @@ export class LoginComponent { } onSubmit(): void { - this.isLoginPending = true; const loginRequest: LoginRequest = this.loginForm.value; - this.userRestService.login(loginRequest) - .then(loginResponse => { - this.matSnackBar.open('Login success!', 'Close', { duration: 5000 }); - }) - .catch(error => { - this.matSnackBar.open('An error occured while login.', 'Close', { duration: 5000 }); - }); + this.loginService.login(loginRequest); } } \ No newline at end of file diff --git a/sportshub-gui/src/app/components/login/login.module.ts b/sportshub-gui/src/app/components/login/login.module.ts index 12e2fd7..0f04280 100644 --- a/sportshub-gui/src/app/components/login/login.module.ts +++ b/sportshub-gui/src/app/components/login/login.module.ts @@ -4,6 +4,7 @@ import {CoreModule} from "../../core/core.module"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {RouterModule} from "@angular/router"; import {HttpClientModule} from "@angular/common/http"; +import {LoginService} from "./login.service"; const routes = [ { @@ -16,6 +17,9 @@ const routes = [ declarations: [ LoginComponent ], + providers: [ + LoginService + ], imports: [ CoreModule, RouterModule.forChild(routes), diff --git a/sportshub-gui/src/app/components/login/login.service.ts b/sportshub-gui/src/app/components/login/login.service.ts index e69de29..2bfdb17 100644 --- a/sportshub-gui/src/app/components/login/login.service.ts +++ b/sportshub-gui/src/app/components/login/login.service.ts @@ -0,0 +1,37 @@ +import {Injectable} from "@angular/core"; +import {UserRestService} from "../../core/rest-services/user.rest-service"; +import {LoginRequest} from "../../core/model/login-request"; +import {Subject} from "rxjs"; +import {MessageService} from "../../core/services/message.service"; +import {AuthenticationService} from "../../core/services/authentication.service"; +import {Router} from "@angular/router"; + +@Injectable() +export class LoginService { + private isLoginPending: Subject = new Subject(); + + constructor( + private authenticationService: AuthenticationService, + private messageService: MessageService, + private router: Router, + private userRestService: UserRestService + ) {} + + login(loginRequest: LoginRequest): void { + this.isLoginPending.next(true); + + this.userRestService.login(loginRequest) + .then(loginResponse => { + this.messageService.display('Login success!'); + this.authenticationService.setAuthenticated(loginResponse); + this.router.navigate(['/']); + }) + .catch(error => { + if (error.status === 400) { + this.messageService.display('Login or password incorrect.') + } else { + this.messageService.display('An error occured while login.') + } + }); + } +} \ No newline at end of file diff --git a/sportshub-gui/src/app/components/logout/logout.component.html b/sportshub-gui/src/app/components/logout/logout.component.html new file mode 100644 index 0000000..6d04722 --- /dev/null +++ b/sportshub-gui/src/app/components/logout/logout.component.html @@ -0,0 +1,2 @@ +

Disconnection...

+ \ No newline at end of file diff --git a/sportshub-gui/src/app/components/logout/logout.component.scss b/sportshub-gui/src/app/components/logout/logout.component.scss new file mode 100644 index 0000000..2596e29 --- /dev/null +++ b/sportshub-gui/src/app/components/logout/logout.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/sportshub-gui/src/app/components/logout/logout.component.ts b/sportshub-gui/src/app/components/logout/logout.component.ts new file mode 100644 index 0000000..a8fd512 --- /dev/null +++ b/sportshub-gui/src/app/components/logout/logout.component.ts @@ -0,0 +1,15 @@ +import {Component, inject, OnInit} from "@angular/core"; +import {LogoutService} from "./logout.service"; + +@Component({ + selector: 'app-logout', + templateUrl: './logout.component.html', + styleUrls: ['./logout.component.scss'] +}) +export class LogoutComponent implements OnInit { + private logoutService = inject(LogoutService); + + ngOnInit(): void { + this.logoutService.logout(); + } +} \ No newline at end of file diff --git a/sportshub-gui/src/app/components/logout/logout.module.ts b/sportshub-gui/src/app/components/logout/logout.module.ts new file mode 100644 index 0000000..a6d4170 --- /dev/null +++ b/sportshub-gui/src/app/components/logout/logout.module.ts @@ -0,0 +1,29 @@ +import {NgModule} from "@angular/core"; +import {RouterModule} from "@angular/router"; +import {LogoutComponent} from "./logout.component"; +import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; +import {LogoutService} from "./logout.service"; + +const routes = [ + { + path: '', + component: LogoutComponent + } +]; + +@NgModule({ + declarations: [ + LogoutComponent + ], + providers: [ + LogoutService + ], + imports: [ + RouterModule.forChild(routes), + MatProgressSpinnerModule + ], + exports: [ + LogoutComponent + ] +}) +export class LogoutModule {} \ No newline at end of file diff --git a/sportshub-gui/src/app/components/logout/logout.service.ts b/sportshub-gui/src/app/components/logout/logout.service.ts new file mode 100644 index 0000000..f75b89e --- /dev/null +++ b/sportshub-gui/src/app/components/logout/logout.service.ts @@ -0,0 +1,16 @@ +import {Injectable} from "@angular/core"; +import {AuthenticationService} from "../../core/services/authentication.service"; +import {Router} from "@angular/router"; + +@Injectable() +export class LogoutService { + constructor( + private authenticationService: AuthenticationService, + private router: Router + ) {} + + logout(): void { + this.authenticationService.setAnonymous(); + this.router.navigate(['/']); + } +} \ No newline at end of file diff --git a/sportshub-gui/src/app/core/services/authentication.service.ts b/sportshub-gui/src/app/core/services/authentication.service.ts new file mode 100644 index 0000000..e2ee861 --- /dev/null +++ b/sportshub-gui/src/app/core/services/authentication.service.ts @@ -0,0 +1,45 @@ +import {Injectable} from "@angular/core"; +import {CookieService} from "ngx-cookie-service"; +import {LoginResponse} from "../model/login-response"; +import {BehaviorSubject, Observable} from "rxjs"; + +const COOKIE_JWT = 'jwt'; +const COOKIE_REFRESH_TOKEN = 'refreshToken'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationService { + private authenticationSubject: BehaviorSubject; + + constructor( + private cookieService: CookieService + ) { + const isAuthenticated = this.isAuthenticated(); + this.authenticationSubject = new BehaviorSubject(isAuthenticated); + } + + get isAuthenticated$(): Observable { + return this.authenticationSubject.asObservable(); + } + + setAuthenticated(loginResponse: LoginResponse): void { + const jwt = loginResponse.accessToken; + this.cookieService.set(COOKIE_JWT, jwt); + + const refreshToken = loginResponse.refreshToken; + this.cookieService.set(COOKIE_REFRESH_TOKEN, refreshToken); + this.authenticationSubject.next(true); + } + + setAnonymous(): void { + this.cookieService.delete(COOKIE_JWT); + this.cookieService.delete(COOKIE_REFRESH_TOKEN); + this.authenticationSubject.next(false); + } + + isAuthenticated(): boolean { + const jwt = this.cookieService.get(COOKIE_JWT); + return jwt?.length > 0; + } +} \ No newline at end of file diff --git a/sportshub-gui/src/app/core/services/message.service.ts b/sportshub-gui/src/app/core/services/message.service.ts new file mode 100644 index 0000000..5fe8926 --- /dev/null +++ b/sportshub-gui/src/app/core/services/message.service.ts @@ -0,0 +1,15 @@ +import {inject, Injectable} from "@angular/core"; +import {MatSnackBar} from "@angular/material/snack-bar"; + +const MESSAGE_DURATION = 5000; + +@Injectable({ + providedIn: 'root' +}) +export class MessageService { + private matSnackBar = inject(MatSnackBar); + + display(message: string): void { + this.matSnackBar.open(message, 'Close', { duration: MESSAGE_DURATION }); + } +} \ No newline at end of file diff --git a/sportshub-gui/src/app/header/header.component.html b/sportshub-gui/src/app/header/header.component.html index aedd2d0..9e5c509 100644 --- a/sportshub-gui/src/app/header/header.component.html +++ b/sportshub-gui/src/app/header/header.component.html @@ -1,4 +1,11 @@ SportsHub \ No newline at end of file diff --git a/sportshub-gui/src/app/header/header.component.scss b/sportshub-gui/src/app/header/header.component.scss index c92fbd6..1f242d7 100644 --- a/sportshub-gui/src/app/header/header.component.scss +++ b/sportshub-gui/src/app/header/header.component.scss @@ -3,6 +3,7 @@ flex-direction: row; justify-content: space-between; padding: .5em 1em; + background-color: #004680; .title { font-size: 2em; @@ -11,7 +12,7 @@ justify-content: center; align-items: center; padding: .5em; - color: darkslategray; + color: white; } #menu { @@ -31,6 +32,11 @@ align-items: center; padding: .5em 1em; text-decoration: none; + gap: .5em; + + &.logout { + background-color: #c20000; + } } } } \ No newline at end of file diff --git a/sportshub-gui/src/app/header/header.component.ts b/sportshub-gui/src/app/header/header.component.ts index 877df4b..a59ac1e 100644 --- a/sportshub-gui/src/app/header/header.component.ts +++ b/sportshub-gui/src/app/header/header.component.ts @@ -1,16 +1,28 @@ import {Component} from "@angular/core"; import {RouterLink} from "@angular/router"; - +import {AuthenticationService} from "../core/services/authentication.service"; +import {Observable} from "rxjs"; +import {AsyncPipe, NgIf} from "@angular/common"; +import {MatIconModule} from "@angular/material/icon"; @Component({ selector: 'app-header', standalone: true, templateUrl: './header.component.html', imports: [ - RouterLink + RouterLink, + AsyncPipe, + NgIf, + MatIconModule ], styleUrls: ['./header.component.scss'] }) export class HeaderComponent { + isAuthenticated$: Observable; + constructor( + private authenticationService: AuthenticationService + ) { + this.isAuthenticated$ = this.authenticationService.isAuthenticated$; + } } \ No newline at end of file