diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index cc9f16a2572084418e8084ac0e8436e89d5fe78e..e7e52edb3be6d27dd87c88827df1c825b5ba2df8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,8 +1,10 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AboutComponent } from './about/about.component'; +import { AuthGuard } from './guards/auth.guard'; import { HomeComponent } from './home/home.component'; import { LegalNoticeComponent } from './legal-notice/legal-notice.component'; +import { ProfileComponent } from './profile/profile.component'; import { StructureDetailsComponent } from './structure-list/components/structure-details/structure-details.component'; import { StructureListComponent } from './structure-list/structure-list.component'; import { UserVerificationComponent } from './user-verification/user-verification.component'; @@ -41,6 +43,11 @@ const routes: Routes = [ path: 'users/verify/:id', component: UserVerificationComponent, }, + { + path: 'profile', + canActivate: [AuthGuard], + component: ProfileComponent, + }, { path: '**', redirectTo: 'home', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 02649bb0928321b50390bc3aee7092d6fcdc9ef5..799d3b558ab712887dfd53b35ca52313f00529dc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,6 @@ import { LOCALE_ID, NgModule } from '@angular/core'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; -import { FlexLayoutModule } from '@angular/flex-layout'; import { AppRoutingModule } from './app-routing.module'; @@ -14,7 +13,6 @@ import { SharedModule } from './shared/shared.module'; import { MapModule } from './map/map.module'; import { StructureListComponent } from './structure-list/structure-list.component'; import { CardComponent } from './structure-list/components/card/card.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SearchComponent } from './structure-list/components/search/search.component'; import { StructureDetailsComponent } from './structure-list/components/structure-details/structure-details.component'; import { StructureOpeningStatusComponent } from './structure-list/components/structure-opening-status/structure-opening-status.component'; @@ -23,6 +21,9 @@ import { LegalNoticeComponent } from './legal-notice/legal-notice.component'; import { AboutComponent } from './about/about.component'; import { MenuPhoneComponent } from './menu-phone/menu-phone.component'; import { UserVerificationComponent } from './user-verification/user-verification.component'; +import { AuthGuard } from './guards/auth.guard'; +import { CustomHttpInterceptor } from './config/http-interceptor'; +import { ProfileModule } from './profile/profile.module'; @NgModule({ declarations: [ @@ -41,17 +42,13 @@ import { UserVerificationComponent } from './user-verification/user-verification MenuPhoneComponent, UserVerificationComponent, ], - imports: [ - BrowserModule, - HttpClientModule, - AppRoutingModule, - FlexLayoutModule, - SharedModule, - MapModule, - FormsModule, - ReactiveFormsModule, + imports: [BrowserModule, HttpClientModule, AppRoutingModule, SharedModule, MapModule, ProfileModule], + providers: [ + { provide: LOCALE_ID, useValue: 'fr' }, + { provide: HTTP_INTERCEPTORS, useClass: CustomHttpInterceptor, multi: true }, + CustomBreakPointsProvider, + AuthGuard, ], - providers: [{ provide: LOCALE_ID, useValue: 'fr' }, CustomBreakPointsProvider], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/config/http-interceptor.ts b/src/app/config/http-interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4911dbb91d27b0ae93f214a1c4d8d52a9d4b2a2 --- /dev/null +++ b/src/app/config/http-interceptor.ts @@ -0,0 +1,38 @@ +import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { HttpInterceptor } from '@angular/common/http'; +import { HttpRequest } from '@angular/common/http'; +import { HttpHandler } from '@angular/common/http'; +import { HttpEvent } from '@angular/common/http'; +import { HttpHeaders } from '@angular/common/http'; +import { AuthService } from '../services/auth.service'; + +@Injectable() +export class CustomHttpInterceptor implements HttpInterceptor { + constructor(private authService: AuthService) {} + + intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + const token = this.authService.token; + let changedRequest = request; + // HttpHeader object immutable - copy values + const headerSettings: { [name: string]: string | string[] } = {}; + + if (request.headers.get('skip')) { + return next.handle(request); + } + + for (const key of request.headers.keys()) { + headerSettings[key] = request.headers.getAll(key); + } + if (token) { + headerSettings['Authorization'] = 'Bearer ' + token; + } + headerSettings['Content-Type'] = 'application/json'; + const newHeader = new HttpHeaders(headerSettings); + + changedRequest = request.clone({ + headers: newHeader, + }); + return next.handle(changedRequest); + } +} diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d3205c2e848f41f2425f3a04bb931ce66c15a63 --- /dev/null +++ b/src/app/guards/auth.guard.ts @@ -0,0 +1,19 @@ +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { AuthService } from '../services/auth.service'; +import { Observable } from 'rxjs'; + +/** + * Guard to assert that we are logged in. Otherwise redirect to home + */ +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) {} + + canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree | boolean { + if (this.authService.isLoggedIn()) { + return true; + } + return this.router.parseUrl('/home'); + } +} diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index f933d061ed0f48e2f2dee5818a41825b88a6139a..863bf211bce5dccdacfba72fcbbb94a3de555c56 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -14,12 +14,15 @@ <a routerLink="/projects" [routerLinkActive]="'active'" i18n>Projets</a> --> <a routerLink="/about" [routerLinkActive]="'active'" i18n>Qui sommes-nous ?</a> <!-- <a routerLink="/login" [routerLinkActive]="'active'" i18n><span class="clickable ico-mglass purple"></span></a> --> - <button *ngIf="!isLoggedIn" (click)="isPopUpOpen = !isPopUpOpen"> + <a *ngIf="!isLoggedIn" (click)="isPopUpOpen = !isPopUpOpen"> <span class="ico-profile" fxLayout="column" fxLayoutAlign="center center"> <span class="head"></span> <span class="body"></span> </span> - </button> + </a> + <a *ngIf="isLoggedIn" routerLink="/profile" [routerLinkActive]="'active'" fxLayout="row" fxLayoutGap="1.5vh"> + <app-svg-icon [type]="'ico'" [iconClass]="'icon-32'" [icon]="'user'" [iconColor]="'currentColor'"></app-svg-icon> + </a> <button *ngIf="isLoggedIn" (click)="logout()">Logout</button> </div> </div> diff --git a/src/app/map/map.module.ts b/src/app/map/map.module.ts index 3487eb49d47035b00a1bd66a07ac50c5feb7fdfa..521ecc6784cc5b12659516889e1526cf69bcb5df 100644 --- a/src/app/map/map.module.ts +++ b/src/app/map/map.module.ts @@ -4,8 +4,9 @@ import { SharedModule } from '../shared/shared.module'; import { MapComponents } from './components'; import { LeafletModule } from '@asymmetrik/ngx-leaflet'; import { NgxLeafletLocateModule } from '@runette/ngx-leaflet-locate'; +import { BrowserModule } from '@angular/platform-browser'; @NgModule({ - imports: [CommonModule, SharedModule, NgxLeafletLocateModule, LeafletModule], + imports: [CommonModule, BrowserModule, SharedModule, NgxLeafletLocateModule, LeafletModule], declarations: [MapComponents], providers: [DatePipe], exports: [MapComponents], diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts index 3269aeea7176afe91ee0e1eab134afc46aae0762..aa2391853d058ee08bd800715ec4bd620220678b 100644 --- a/src/app/models/user.model.ts +++ b/src/app/models/user.model.ts @@ -1,7 +1,7 @@ export class User { _id: string; email: string; - password: string; + password?: string; emailVerified: boolean; role: number; validationToken: string; diff --git a/src/app/profile/profile.component.html b/src/app/profile/profile.component.html new file mode 100644 index 0000000000000000000000000000000000000000..1a7ca9883fc528448dad988b6b2b1e23ae907cbc --- /dev/null +++ b/src/app/profile/profile.component.html @@ -0,0 +1,61 @@ +<div fxLayout="column" class="content-container"> + <div class="section-container" fxLayout="column" fxLayoutAlign="center center"> + <h1>Profil</h1> + <div *ngIf="userProfile" fxLayout="column" fxLayoutAlign="center" fxLayoutGap="10px"> + <p>Id: {{ userProfile._id }}</p> + <p>Email: {{ userProfile.email }}</p> + <button (click)="toogleChangePassword()">Changer de mot de passe</button> + + <form *ngIf="changePassword" fxLayout="column" fxLayoutGap="10px" [formGroup]="form" (ngSubmit)="onSubmit()"> + <div class="form-group"> + <label for="oldPassword">Ancien mot de passe</label> + <input + type="password" + autocomplete="on" + formControlName="oldPassword" + class="form-control" + [ngClass]="{ 'is-invalid': submitted && f.oldPassword.errors }" + /> + <div *ngIf="submitted && f.oldPassword.errors" class="invalid-feedback"> + <div *ngIf="f.oldPassword.errors.required">L'Ancien mot de passe est obligatoire</div> + </div> + </div> + <div class="form-group"> + <label for="password">Mot de passe</label> + <input + type="password" + autocomplete="on" + formControlName="password" + class="form-control" + [ngClass]="{ 'is-invalid': submitted && f.password.errors }" + /> + <div *ngIf="submitted && f.password.errors" class="invalid-feedback"> + <div *ngIf="f.password.errors.required">Le mot de passe est obligatoire</div> + <div *ngIf="f.password.errors.pattern"> + Le mot de passe doit avoir au minimun 8 caractères, une majuscule, une minuscule, un chiffre et un + caractère spécial. + </div> + </div> + </div> + <div class="form-group"> + <label for="confirmPassword">Confirmation du mot de passe</label> + <input + type="password" + autocomplete="on" + formControlName="confirmPassword" + class="form-control" + [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" + /> + <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback"> + <div *ngIf="f.confirmPassword.errors.required">La confirmation du mot de passe est obligatoire</div> + <div *ngIf="f.confirmPassword.errors.mustMatch">Les mot de passe ne sont pas les mêmes</div> + </div> + </div> + + <div class="form-group"> + <button type="submit" [disabled]="loading" class="btn btn-primary">Appliquer</button> + </div> + </form> + </div> + </div> +</div> diff --git a/src/app/profile/profile.component.scss b/src/app/profile/profile.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/profile/profile.component.spec.ts b/src/app/profile/profile.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e88012e7aeab34312f88c177994686deb064e20b --- /dev/null +++ b/src/app/profile/profile.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileComponent } from './profile.component'; + +describe('ProfileComponent', () => { + let component: ProfileComponent; + let fixture: ComponentFixture<ProfileComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProfileComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/profile/profile.component.ts b/src/app/profile/profile.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e22fff776015432069060bdb2ae221e83fc0fd78 --- /dev/null +++ b/src/app/profile/profile.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { User } from '../models/user.model'; +import { MustMatch } from '../shared/validator/form'; +import { ProfileService } from './services/profile.service'; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.scss'], +}) +export class ProfileComponent implements OnInit { + public form: FormGroup; + public userProfile: User; + public submitted = false; + public changePassword = false; + public loading = false; + + constructor(private formBuilder: FormBuilder, private profileService: ProfileService) {} + + ngOnInit(): void { + this.profileService.getProfile().subscribe((profile) => { + this.userProfile = profile; + }); + this.initForm(); + } + + public initForm(): void { + this.form = this.formBuilder.group( + { + oldPassword: [ + '', + [Validators.required, Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/)], + ], + password: [ + '', + [Validators.required, Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/)], + ], + confirmPassword: [''], + }, + { validator: MustMatch('password', 'confirmPassword') } + ); + } + + // getter for form fields + get f(): { [key: string]: AbstractControl } { + return this.form.controls; + } + + public toogleChangePassword(): void { + this.changePassword = !this.changePassword; + } + + public onSubmit(): void { + this.submitted = true; + // stop here if form is invalid + if (this.form.invalid) { + return; + } + this.loading = true; + + this.profileService.changePassword(this.form.value.password, this.form.value.oldPassword).subscribe( + () => { + this.toogleChangePassword(); + }, + (error) => { + this.loading = false; + } + ); + } +} diff --git a/src/app/profile/profile.module.ts b/src/app/profile/profile.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9427003f2507d4092e966617363aa223996b4f5 --- /dev/null +++ b/src/app/profile/profile.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { ProfileComponent } from './profile.component'; +import { SharedModule } from '../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; + +@NgModule({ + imports: [CommonModule, BrowserModule, SharedModule], + declarations: [ProfileComponent], + exports: [ProfileComponent], +}) +export class ProfileModule {} diff --git a/src/app/profile/services/profile.service.ts b/src/app/profile/services/profile.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e1a65cf28b119a03a4b19debaee3a6543faedbd --- /dev/null +++ b/src/app/profile/services/profile.service.ts @@ -0,0 +1,21 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { User } from '../../models/user.model'; + +@Injectable({ + providedIn: 'root', +}) +export class ProfileService { + private readonly baseUrl = 'api/users'; + + constructor(private http: HttpClient) {} + + public getProfile(): Observable<User> { + return this.http.get<User>(`${this.baseUrl}/profile`); + } + + public changePassword(newPassword: string, oldPassword: string): Observable<User> { + return this.http.post<any>(`${this.baseUrl}/change-password`, { newPassword, oldPassword }); + } +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 9834bd0077a68ea597950c43c3f40ba3dc93d3b2..4f40f2c3de4bac22ba01f1723f73e3f9d68ce71b 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -22,7 +22,10 @@ export class AuthService { } public get token(): string { - return this.userSubject.value.accessToken; + if (this.userSubject.value) { + return this.userSubject.value.accessToken; + } + return null; } public logout(): void { diff --git a/src/app/services/geojson.service.ts b/src/app/services/geojson.service.ts index b48ce0da4b6dd0a1d46bc8557607893691bb2003..ac3bfe1e2cedf2c0375deda7deadfde9112ab341 100644 --- a/src/app/services/geojson.service.ts +++ b/src/app/services/geojson.service.ts @@ -17,7 +17,7 @@ export class GeojsonService { */ public getAddressByCoord(longitude: number, latitude: number): Observable<any> { return this.http - .get('/reverse/' + '?lon=' + longitude + '&lat=' + latitude) + .get('/reverse/' + '?lon=' + longitude + '&lat=' + latitude, { headers: { skip: 'true' } }) .pipe(map((data: { features: any[] }) => new GeoJson(data.features[0]))); } @@ -37,7 +37,8 @@ export class GeojsonService { return this.http .get( '/wfs/grandlyon' + - '?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=ter_territoire.maison_de_la_metropole&outputFormat=application/json; subtype=geojson&SRSNAME=EPSG:4171&startIndex=0' + '?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=ter_territoire.maison_de_la_metropole&outputFormat=application/json; subtype=geojson&SRSNAME=EPSG:4171&startIndex=0', + { headers: { skip: 'true' } } ) .pipe(map((data: { features: any[] }) => _.map(data.features, this.parseToGeoJson))); } @@ -48,7 +49,7 @@ export class GeojsonService { */ public getCoord(numero: string, address: string, zipcode: string): Observable<GeoJson> { return this.http - .get('/geocoding/photon/api' + '?q=' + numero + ' ' + address + ' ' + zipcode) + .get('/geocoding/photon/api' + '?q=' + numero + ' ' + address + ' ' + zipcode, { headers: { skip: 'true' } }) .pipe(map((data: { features: any[]; type: string }) => new GeoJson(data.features[0]))); } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 3acb5e40b1e85264b0188e786f40b0c3bd29e411..7a91eef73550ffb2f1efc354f8f4bf35e26ce7a7 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { FlexLayoutModule } from '@angular/flex-layout'; import { SharedComponents } from './components'; @@ -10,6 +10,15 @@ import { SvgIconComponent } from './components/svg-icon/svg-icon.component'; @NgModule({ imports: [CommonModule, RouterModule, FlexLayoutModule, ReactiveFormsModule], declarations: [...SharedPipes, ...SharedComponents, ...SharedDirectives, SvgIconComponent], - exports: [...SharedPipes, ...SharedComponents, ...SharedDirectives], + exports: [ + ...SharedPipes, + ...SharedComponents, + ...SharedDirectives, + CommonModule, + RouterModule, + FlexLayoutModule, + FormsModule, + ReactiveFormsModule, + ], }) export class SharedModule {}