Implementation of login and logout mechanisms.

This commit is contained in:
Florian THIERRY
2023-12-05 14:31:07 +01:00
parent c095cdab3a
commit 9f40a6c782
17 changed files with 228 additions and 13 deletions

View File

@@ -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",

View File

@@ -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 @@
"<rootDir>/src/setupJest.ts"
]
}
}
}

View File

@@ -0,0 +1,8 @@
:host {
display: flex;
flex-direction: column;
app-header {
margin-bottom: 1em;
}
}

View File

@@ -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)
}
];

View File

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

View File

@@ -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),

View File

@@ -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<boolean> = new Subject<boolean>();
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.')
}
});
}
}

View File

@@ -0,0 +1,2 @@
<h1>Disconnection...</h1>
<mat-spinner></mat-spinner>

View File

@@ -0,0 +1,6 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

View File

@@ -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();
}
}

View File

@@ -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 {}

View File

@@ -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(['/']);
}
}

View File

@@ -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<boolean>;
constructor(
private cookieService: CookieService
) {
const isAuthenticated = this.isAuthenticated();
this.authenticationSubject = new BehaviorSubject<boolean>(isAuthenticated);
}
get isAuthenticated$(): Observable<boolean> {
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;
}
}

View File

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

View File

@@ -1,4 +1,11 @@
<a class="title" routerLink="/">SportsHub</a>
<div id="menu">
<a routerLink="/login">Login</a>
<a routerLink="/login" *ngIf="(isAuthenticated$ | async) === false">
<mat-icon>login</mat-icon>
Login
</a>
<a routerLink="/logout" *ngIf="isAuthenticated$ | async" class="logout">
<mat-icon>logout</mat-icon>
Logout
</a>
</div>

View File

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

View File

@@ -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<boolean>;
constructor(
private authenticationService: AuthenticationService
) {
this.isAuthenticated$ = this.authenticationService.isAuthenticated$;
}
}