From 48e36f2541e41510334eee221d54c7277a12cdcb Mon Sep 17 00:00:00 2001 From: Bastyen Date: Tue, 9 Jul 2024 15:34:47 +0200 Subject: [PATCH] add api services --- front-end/src/app/app.component.ts | 12 ++- front-end/src/app/app.config.ts | 13 ++- front-end/src/app/app.routes.ts | 1 - .../src/app/interceptors/http.interceptor.ts | 90 +++++++++++++++++++ .../create-account.component.html | 14 +++ .../create-account.component.ts | 27 +++++- .../forget-password.component.html | 2 +- .../src/app/pages/login/login.component.ts | 25 +++++- .../my-account/my-account.component.html | 8 ++ .../pages/my-account/my-account.component.ts | 30 +++++-- .../synthesis-interface.component.ts | 26 ++++-- front-end/src/app/services/auth.service.ts | 86 ++++++++++++++++-- .../app/services/observations.service.spec.ts | 16 ++++ .../src/app/services/observations.service.ts | 30 +++++++ .../src/app/services/settings.service.spec.ts | 16 ++++ .../src/app/services/settings.service.ts | 23 +++++ 16 files changed, 386 insertions(+), 33 deletions(-) create mode 100644 front-end/src/app/interceptors/http.interceptor.ts create mode 100644 front-end/src/app/services/observations.service.spec.ts create mode 100644 front-end/src/app/services/observations.service.ts create mode 100644 front-end/src/app/services/settings.service.spec.ts create mode 100644 front-end/src/app/services/settings.service.ts diff --git a/front-end/src/app/app.component.ts b/front-end/src/app/app.component.ts index 3aa8ccc..ab8d2eb 100644 --- a/front-end/src/app/app.component.ts +++ b/front-end/src/app/app.component.ts @@ -18,11 +18,12 @@ import { import { MatListModule } from '@angular/material/list'; import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav'; import { Location } from '@angular/common'; -import { AuthService } from './services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { Platform } from '@angular/cdk/platform'; import { MatBadgeModule } from '@angular/material/badge'; import { MatDividerModule } from '@angular/material/divider'; import { OfflineService } from './services/offline.service'; +import { AuthService } from './services/auth.service'; @Component({ selector: 'app-root', @@ -38,6 +39,7 @@ import { OfflineService } from './services/offline.service'; MatListModule, MatBadgeModule, MatDividerModule, + MatSnackBarModule, ], templateUrl: './app.component.html', styleUrl: './app.component.scss', @@ -52,6 +54,7 @@ export class AppComponent { location = inject(Location); authService = inject(AuthService); offlineService = inject(OfflineService); + snackBar = inject(MatSnackBar); @ViewChild('sidenav') private sidenav!: MatSidenav; @@ -112,6 +115,8 @@ export class AppComponent { click: () => { this.authService.logout(); this.sidenav.close(); + this.snackBar.open('Vous êtes déconnecté', '', { duration: 2000 }); + this.router.navigate(['..']); }, observationsPending: false, }, @@ -144,6 +149,11 @@ export class AppComponent { ngOnInit() { this.authService.isAuth.subscribe((value) => { + if (value) { + this.authService.getAccount().subscribe((account) => { + console.log(account); + }); + } this.handleAuthentification(value); }); this.router.events.subscribe((event) => { diff --git a/front-end/src/app/app.config.ts b/front-end/src/app/app.config.ts index 82042cc..72b9526 100644 --- a/front-end/src/app/app.config.ts +++ b/front-end/src/app/app.config.ts @@ -10,9 +10,17 @@ import { provideClientHydration } from '@angular/platform-browser'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { MAT_DATE_LOCALE } from '@angular/material/core'; import { provideMomentDateAdapter } from '@angular/material-moment-adapter'; -import { provideHttpClient, withFetch } from '@angular/common/http'; +import { + provideHttpClient, + withFetch, + withInterceptorsFromDi, +} from '@angular/common/http'; import { provideServiceWorker } from '@angular/service-worker'; +import { httpInterceptorProviders } from './interceptors/http.interceptor'; +export function tokenGetter() { + return localStorage.getItem('access_token'); +} export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), @@ -31,7 +39,8 @@ export const appConfig: ApplicationConfig = { monthYearA11yLabel: 'MMMM YYYY', }, }), - provideHttpClient(withFetch()), + httpInterceptorProviders, + provideHttpClient(withInterceptorsFromDi(), withFetch()), provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), registrationStrategy: 'registerWhenStable:30000', diff --git a/front-end/src/app/app.routes.ts b/front-end/src/app/app.routes.ts index 1ffee9f..2ad93e5 100644 --- a/front-end/src/app/app.routes.ts +++ b/front-end/src/app/app.routes.ts @@ -91,7 +91,6 @@ export const routes: Routes = [ backButton: true, accountButton: false, }, - canActivate: [authGuard], }, { path: 'mon-compte', diff --git a/front-end/src/app/interceptors/http.interceptor.ts b/front-end/src/app/interceptors/http.interceptor.ts new file mode 100644 index 0000000..622dbfe --- /dev/null +++ b/front-end/src/app/interceptors/http.interceptor.ts @@ -0,0 +1,90 @@ +import { inject, Injectable } from '@angular/core'; +import { + HttpEvent, + HttpInterceptor, + HttpHandler, + HttpRequest, + HTTP_INTERCEPTORS, + HttpErrorResponse, +} from '@angular/common/http'; +import { + BehaviorSubject, + catchError, + filter, + Observable, + switchMap, + take, + throwError, +} from 'rxjs'; +import { AuthService } from '../services/auth.service'; + +const TOKEN_HEADER_KEY = 'Authorization'; +@Injectable() +export class HttpRequestInterceptor implements HttpInterceptor { + isRefreshing = false; + refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null); + + authService = inject(AuthService); + + intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { + if (req.url.includes('account') && !req.url.includes('sign-up')) { + const token = this.authService.getToken(); + if (token) { + req = this.addTokenHeader(req, token); + } + } + + return next.handle(req).pipe( + catchError((error) => { + if (error instanceof HttpErrorResponse && error.status === 401) { + return this.handle401Error(req, next); + } + return throwError(() => new Error('Erreur')); + }), + ); + } + + handle401Error(req: HttpRequest, next: HttpHandler) { + if (!this.isRefreshing) { + this.isRefreshing = true; + this.refreshTokenSubject.next(null); + + const refreshToken = this.authService.getRefreshToken(); + + if (refreshToken) { + return this.authService.refreshToken(refreshToken).pipe( + switchMap((token: any) => { + this.isRefreshing = false; + this.authService.saveToken(token.accessToken); + + return next.handle(this.addTokenHeader(req, token.accessToken)); + }), + catchError(() => { + this.isRefreshing = false; + this.authService.logout(); + return throwError(() => new Error('Erreur')); + }), + ); + } + } + + return this.refreshTokenSubject.pipe( + filter((token) => token !== null), + take(1), + switchMap((token) => next.handle(this.addTokenHeader(req, token))), + ); + } + + addTokenHeader(req: HttpRequest, token: string) { + return req.clone({ + headers: req.headers.set(TOKEN_HEADER_KEY, `Bearer ${token}`), + }); + } +} + +export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }, +]; diff --git a/front-end/src/app/pages/create-account/create-account.component.html b/front-end/src/app/pages/create-account/create-account.component.html index 5630978..eb5340c 100644 --- a/front-end/src/app/pages/create-account/create-account.component.html +++ b/front-end/src/app/pages/create-account/create-account.component.html @@ -12,6 +12,20 @@ Une adresse email est nécessaire } + + Nom + + @if (lastNameFormControl.hasError("required")) { + Un nom nécessaire + } + + + Prénom + + @if (emailFormControl.hasError("required")) { + Un prénom nécessaire + } + Mot de passe diff --git a/front-end/src/app/pages/create-account/create-account.component.ts b/front-end/src/app/pages/create-account/create-account.component.ts index bed6af6..94a2d2f 100644 --- a/front-end/src/app/pages/create-account/create-account.component.ts +++ b/front-end/src/app/pages/create-account/create-account.component.ts @@ -10,6 +10,8 @@ import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-create-account', @@ -22,6 +24,7 @@ import { Router, RouterLink } from '@angular/router'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './create-account.component.html', styleUrl: './create-account.component.scss', @@ -31,6 +34,8 @@ export class CreateAccountComponent { Validators.required, Validators.email, ]); + firstNameFormControl = new FormControl('', [Validators.required]); + lastNameFormControl = new FormControl('', [Validators.required]); passwordFormControl = new FormControl('', [ Validators.required, Validators.minLength(6), @@ -43,6 +48,8 @@ export class CreateAccountComponent { passwordError = false; router = inject(Router); + authService = inject(AuthService); + snackBar = inject(MatSnackBar); ngOnInit() { this.passwordFormControl.valueChanges.subscribe((value) => { @@ -67,10 +74,28 @@ export class CreateAccountComponent { this.passwordError = false; if ( this.emailFormControl.valid && + this.firstNameFormControl.valid && + this.lastNameFormControl.valid && this.passwordFormControl.valid && this.confirmPasswordFormControl.valid ) { - this.router.navigate(['/se-connecter']); + this.authService + .createAccount({ + email: this.emailFormControl.value!, + last_name: this.lastNameFormControl.value!, + first_name: this.firstNameFormControl.value!, + password: this.passwordFormControl.value!, + }) + .subscribe({ + next: (success) => { + console.log('success', success); + this.snackBar.open('Compte créé', '', { duration: 2000 }); + this.router.navigate(['/se-connecter']); + }, + error: (error) => { + console.log('error', error); + }, + }); } } else { this.passwordError = true; diff --git a/front-end/src/app/pages/forget-password/forget-password.component.html b/front-end/src/app/pages/forget-password/forget-password.component.html index 57b38ca..37d1549 100644 --- a/front-end/src/app/pages/forget-password/forget-password.component.html +++ b/front-end/src/app/pages/forget-password/forget-password.component.html @@ -12,7 +12,7 @@ Une adresse email est nécessaire } - diff --git a/front-end/src/app/pages/login/login.component.ts b/front-end/src/app/pages/login/login.component.ts index e5ca340..598653e 100644 --- a/front-end/src/app/pages/login/login.component.ts +++ b/front-end/src/app/pages/login/login.component.ts @@ -10,6 +10,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-login', @@ -21,6 +22,7 @@ import { AuthService } from '../../services/auth.service'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './login.component.html', styleUrl: './login.component.scss', @@ -37,13 +39,28 @@ export class LoginComponent { router = inject(Router); authService = inject(AuthService); + snackBar = inject(MatSnackBar); onLogin() { if (this.emailFormControl.valid && this.passwordFormControl.valid) { - this.authService.login(); - if (this.authService.isAuth.value) { - this.router.navigate(['..']); - } + this.authService + .login({ + email: this.emailFormControl.value!, + password: this.passwordFormControl.value!, + }) + .subscribe({ + next: (success: any) => { + console.log('success', success); + this.snackBar.open('Vous êtes connecté', '', { duration: 2000 }); + this.authService.saveToken(success.access); + this.authService.saveRefreshToken(success.refresh); + this.authService.checkAuth(); + this.router.navigate(['..']); + }, + error: (error) => { + console.log('error', error); + }, + }); } } } diff --git a/front-end/src/app/pages/my-account/my-account.component.html b/front-end/src/app/pages/my-account/my-account.component.html index eda628d..0f0a780 100644 --- a/front-end/src/app/pages/my-account/my-account.component.html +++ b/front-end/src/app/pages/my-account/my-account.component.html @@ -3,6 +3,14 @@ Email + + Nom + + + + Prénom + +
diff --git a/front-end/src/app/pages/my-account/my-account.component.ts b/front-end/src/app/pages/my-account/my-account.component.ts index 66d078f..257cbc5 100644 --- a/front-end/src/app/pages/my-account/my-account.component.ts +++ b/front-end/src/app/pages/my-account/my-account.component.ts @@ -1,10 +1,5 @@ import { Component, inject } from '@angular/core'; -import { - FormControl, - FormGroup, - FormsModule, - ReactiveFormsModule, -} from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -12,6 +7,7 @@ import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; import { DeleteAccountDialog } from './dialogs/delete-account-dialog'; import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-my-account', @@ -23,6 +19,7 @@ import { AuthService } from '../../services/auth.service'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './my-account.component.html', styleUrl: './my-account.component.scss', @@ -30,16 +27,33 @@ import { AuthService } from '../../services/auth.service'; export class MyAccountComponent { readonly dialog = inject(MatDialog); email = 'adresse@email.com'; + lastName = 'Nom'; + firstName = 'Prénom'; + authService = inject(AuthService); router = inject(Router); + snackBar = inject(MatSnackBar); openDeleteAccountDialog() { const deleteDialogRef = this.dialog.open(DeleteAccountDialog); deleteDialogRef.afterClosed().subscribe((result) => { if (result && result.delete) { - this.authService.logout(); - this.router.navigate(['..']); + this.authService.deleteAccount().subscribe({ + next: (success: any) => { + console.log('success', success); + this.authService.removeToken(); + this.authService.removeRefreshToken(); + this.authService.checkAuth(); + this.snackBar.open('Votre compte est supprimé', '', { + duration: 2000, + }); + this.router.navigate(['..']); + }, + error: (error) => { + console.log('error', error); + }, + }); } }); } diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts index e57c9e1..cbfbb54 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts @@ -23,6 +23,7 @@ import { Observations } from '../../types/types'; import evenementsRemarquables from '../../../data/evenements_remarquables.json'; import observationTypes from '../../../data/types.json'; +import { ObservationsService } from '../../services/observations.service'; @Component({ selector: 'app-synthesis-interface', @@ -57,6 +58,7 @@ export class SynthesisInterfaceComponent { observationsLayer: L.GeoJSON | null = null; observationsClusterGroup: any; router = inject(Router); + observationsService = inject(ObservationsService); expansionPanelIsOpen = false; bounds: any; @@ -74,21 +76,27 @@ export class SynthesisInterfaceComponent { } async initMap() { + this.observationsService.getObservations().subscribe({ + next: (success) => { + console.log('success', success); + }, + error: (error) => { + console.log('error', error); + }, + }); this.L = await import('leaflet'); await import('leaflet.locatecontrol'); await import('leaflet.markercluster'); - const tileLayerOffline = await import('leaflet.offline'); + const { tileLayerOffline } = await import('leaflet.offline'); this.map = this.L.default.map('map', { zoom: 4, center: [47, 2] }); - this.L.default - .tileLayerOffline( - 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', - { - attribution: "IGN", - }, - ) - .addTo(this.map); + tileLayerOffline( + 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', + { + attribution: "IGN", + }, + ).addTo(this.map); this.L.default.control .locate({ setView: 'once', showPopup: false }) diff --git a/front-end/src/app/services/auth.service.ts b/front-end/src/app/services/auth.service.ts index c743835..2842387 100644 --- a/front-end/src/app/services/auth.service.ts +++ b/front-end/src/app/services/auth.service.ts @@ -1,7 +1,12 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; +import { environment } from '../../environments/environment'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), +}; @Injectable({ providedIn: 'root', @@ -11,16 +16,85 @@ export class AuthService { isAuth = new BehaviorSubject(false); checkAuth() { - this.isAuth.next(localStorage.getItem('token') !== null); + this.isAuth.next(localStorage.getItem('access_token') !== null); } - login() { - localStorage.setItem('token', 'token'); - this.checkAuth(); + getAccount() { + return this.httpClient.get( + `${environment.apiUrl}/api/accounts/me/`, + httpOptions, + ); + } + + login(account: { email: string; password: string }) { + return this.httpClient.post( + `${environment.apiUrl}/api/token/`, + account, + httpOptions, + ); } logout() { - localStorage.removeItem('token'); + this.removeToken(); + this.removeRefreshToken(); this.checkAuth(); } + + createAccount(account: { + email: string; + last_name: string; + first_name: string; + password: string; + }) { + return this.httpClient.post( + `${environment.apiUrl}/api/accounts/sign-up/`, + account, + httpOptions, + ); + } + + deleteAccount() { + return this.httpClient.delete( + `${environment.apiUrl}/api/accounts/me/`, + httpOptions, + ); + } + + changePassword() {} + + resetPassword() {} + + refreshToken(token: string) { + return this.httpClient.post( + `${environment.apiUrl}/api/token/refresh/`, + { + refreshToken: token, + }, + httpOptions, + ); + } + + getToken() { + return localStorage.getItem('access_token'); + } + + getRefreshToken() { + return localStorage.getItem('refresh_token'); + } + + saveToken(token: string) { + localStorage.setItem('access_token', token); + } + + saveRefreshToken(token: string) { + localStorage.setItem('refresh_token', token); + } + + removeToken() { + localStorage.removeItem('access_token'); + } + + removeRefreshToken() { + localStorage.removeItem('refresh_token'); + } } diff --git a/front-end/src/app/services/observations.service.spec.ts b/front-end/src/app/services/observations.service.spec.ts new file mode 100644 index 0000000..517bc98 --- /dev/null +++ b/front-end/src/app/services/observations.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ObservationsService } from './observations.service'; + +describe('ObservationsService', () => { + let service: ObservationsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ObservationsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/front-end/src/app/services/observations.service.ts b/front-end/src/app/services/observations.service.ts new file mode 100644 index 0000000..48ddecf --- /dev/null +++ b/front-end/src/app/services/observations.service.ts @@ -0,0 +1,30 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), +}; + +@Injectable({ + providedIn: 'root', +}) +export class ObservationsService { + httpClient = inject(HttpClient); + + constructor() {} + + getObservations() { + return this.httpClient.get( + `${environment.apiUrl}/api/observations/`, + httpOptions, + ); + } + + getObservation(observationId: number) { + return this.httpClient.get( + `${environment.apiUrl}/api/observations/${observationId}/`, + httpOptions, + ); + } +} diff --git a/front-end/src/app/services/settings.service.spec.ts b/front-end/src/app/services/settings.service.spec.ts new file mode 100644 index 0000000..359cb6b --- /dev/null +++ b/front-end/src/app/services/settings.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SettingsService } from './settings.service'; + +describe('SettingsService', () => { + let service: SettingsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SettingsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/front-end/src/app/services/settings.service.ts b/front-end/src/app/services/settings.service.ts new file mode 100644 index 0000000..b6816e2 --- /dev/null +++ b/front-end/src/app/services/settings.service.ts @@ -0,0 +1,23 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), +}; + +@Injectable({ + providedIn: 'root', +}) +export class SettingsService { + httpClient = inject(HttpClient); + + constructor() {} + + getSettings() { + return this.httpClient.get( + `${environment.apiUrl}/api/settings/`, + httpOptions, + ); + } +}